Пример #1
0
    async def from_md5(cls, md5: str, set_id: Optional[int] = None):
        """Create a beatmap object from sql or osu!api using it's md5."""
        # Check if the map is in the cache.
        if md5 in glob.cache['beatmap']:
            # Check if our cached result is within timeout.
            cached = glob.cache['beatmap'][md5]

            if (time.time() - cached['timeout']) <= 0:
                # Cache is within timeout.
                return cached['map']

            # Cache is outdated and should be deleted.
            del glob.cache['beatmap'][md5]

        # Check if the map is in the unsubmitted cache.
        if md5 in glob.cache['unsubmitted']:
            return

        # Try to get from sql.
        if not (m := await cls.from_md5_sql(md5)):
            # Map not found in sql.

            # If the user has no API key, we cannot make
            # any further attempts to serve them the map.
            if not glob.config.osu_api_key:
                plog('Fetching beatmap requires osu!api key.', Ansi.LRED)
                return

            # Try to get from the osu!api.
            if not (m := await cls.from_md5_osuapi(md5, set_id)):
                return
Пример #2
0
    async def remove(self, m: Match) -> None:
        for idx, i in enumerate(self.matches):
            if m == i:
                self.matches[idx] = None
                break

        if glob.config.debug:
            plog(f'{m} removed from matches list.')
Пример #3
0
    async def add(self, c: Channel) -> None:
        if c in self.channels:
            plog(f'{c} double-added to channels list?')
            return

        self.channels.append(c)

        if glob.config.debug:
            plog(f'{c} added to channels list.')
Пример #4
0
    async def add(self, p: Player) -> None:
        if p in self.players:
            if glob.config.debug:
                plog(f'{p} double-added to players list?')
            return

        self.players.append(p)

        if glob.config.debug:
            plog(f'{p} added to players list.')
Пример #5
0
async def osuScreenshot(conn: AsyncConnection) -> Optional[bytes]:
    if 'ss' not in conn.files:
        plog(f'screenshot req missing file.', Ansi.LRED)
        return

    pname = unquote(conn.multipart_args['u'])
    phash = conn.multipart_args['p']

    if not (p := await glob.players.get_login(pname, phash)):
        return
Пример #6
0
    async def add_friend(self, p) -> None:
        if p.id in self.friends:
            plog(f'{self} tried to add {p}, who is already their friend!')
            return

        self.friends.add(p.id)
        await glob.db.execute('INSERT INTO friendships '
                              'VALUES (%s, %s)', [self.id, p.id])

        plog(f'{self} added {p} to their friends.')
Пример #7
0
    async def remove_friend(self, p) -> None:
        if not p.id in self.friends:
            plog(f'{self} tried to remove {p}, who is not their friend!')
            return

        self.friends.remove(p.id)
        await glob.db.execute(
            'DELETE FROM friendships '
            'WHERE user1 = %s AND user2 = %s', [self.id, p.id])

        plog(f'{self} removed {p} from their friends.')
Пример #8
0
        async def _handler(conn: AsyncConnection):
            # make sure we have all required args
            if not all(x in conn.args for x in required_args):
                plog(f'{uri} request missing args.', Ansi.LRED)
                return

            # make sure we have all required multipart args
            if not all(x in conn.multipart_args for x in required_mpargs):
                plog(f'{uri} request missing mpargs.', Ansi.LRED)
                return

            return await cb(conn)
Пример #9
0
    async def add(self, m: Match) -> None:
        if m in self.matches:
            plog(f'{m} double-added to matches list?')
            return

        if (free := self.get_free()) is not None:
            # set the id of the match
            # to our free slot found.
            m.id = free
            self.matches[free] = m

            if glob.config.debug:
                plog(f'{m} added to matches list.')
Пример #10
0
    async def read_packet_header(self) -> None:
        ldata = len(self.data)

        if ldata < 7:
            # Packet not even minimal legnth.
            # End the connection immediately.
            self.current_packet = None
            self._offset += ldata
            plog(f'[ERR] Data misread! (len: {len(self.data)})', Ansi.LRED)
            return

        packet_id, self.length = struct.unpack('<HxI', self.data[:7])
        self.current_packet = BanchoPacket(packet_id)

        self._offset += 7 # read first 7 bytes for packetid & length
Пример #11
0
    async def fetch_geoloc(self, ip: str) -> None:
        """Fetch a player's geolocation data based on their ip."""
        async with glob.http.get(f'http://ip-api.com/json/{ip}') as resp:
            if not resp or resp.status != 200:
                plog('Failed to get geoloc data: request failed.', Ansi.LRED)
                return

            res = await resp.json()

        if 'status' not in res or res['status'] != 'success':
            plog(f"Failed to get geoloc data: {res['message']}.", Ansi.LRED)
            return

        country = res['countryCode']

        self.country = (country_codes[country], country)
        self.location = (res['lon'], res['lat'])
Пример #12
0
    async def restrict(self) -> None:  # TODO: reason
        self.priv &= ~Privileges.Normal
        await glob.db.execute('UPDATE users SET priv = %s WHERE id = %s',
                              [int(self.priv), self.id])

        if self in glob.players:
            # If user is online, notify and log them out.
            # XXX: If you want to lock the player's
            # client, you can send -3 rather than -1.
            self.enqueue(await packets.userID(-1))
            self.enqueue(await packets.notification(
                'Your account has been banned.\n\n'
                'If you believe this was a mistake or '
                'have waited >= 2 months, you can appeal '
                'using the appeal form on the website.'))

        plog(f'Restricted {self}.', Ansi.CYAN)
Пример #13
0
    async def from_submission(cls, data_enc: str, iv: str, osu_ver: str,
                              phash: str) -> None:
        """Create a score object from an osu! submission string."""
        cbc = RijndaelCbc(f'osu!-scoreburgr---------{osu_ver}',
                          iv=base64.b64decode(iv).decode('latin_1'),
                          padding=ZeroPadding(32),
                          block_size=32)

        data = cbc.decrypt(
            base64.b64decode(data_enc).decode('latin_1')).decode().split(':')

        if len(data) != 18:
            plog('Received an invalid score submission.', Ansi.LRED)
            return

        s = cls()

        if len(map_md5 := data[0]) != 32:
            return
Пример #14
0
    async def leave_channel(self, c: Channel) -> None:
        if self not in c:
            plog(f'{self} tried to leave {c} but is not in it.')
            return

        await c.remove(self)  # Remove from channels
        self.channels.remove(c)  # Remove from player

        self.enqueue(await packets.channelKick(c.name))

        # Update channel usercounts for all clients that can see.
        # For instanced channels, enqueue update to only players
        # in the instance; for normal channels, enqueue to all.
        targets = c.players if c.instance else glob.players

        for p in targets:
            p.enqueue(await packets.channelInfo(*c.basic_info))

        if glob.config.debug:
            plog(f'{self} left {c}.')
Пример #15
0
    async def stats_from_sql(self, mode: GameMode) -> None:
        """Fetch the player's stats for a specified gamemode."""
        res = await glob.db.fetch(
            'SELECT tscore_{0:sql} tscore, rscore_{0:sql} rscore, '
            'pp_{0:sql} pp, plays_{0:sql} plays, acc_{0:sql} acc, '
            'playtime_{0:sql} playtime, maxcombo_{0:sql} max_combo '
            'FROM stats WHERE id = %s'.format(mode), [self.id])

        if not res:
            plog(f"Failed to fetch {self}'s {mode!r} user stats.", Ansi.LRED)
            return

        # Calculate rank.
        res['rank'] = await glob.db.fetch(
            'SELECT COUNT(*) AS c FROM stats '
            'LEFT JOIN users USING(id) '
            f'WHERE pp_{mode:sql} > %s '
            'AND priv & 1', [res['pp']])['c']

        self.stats[mode].update(**res)
Пример #16
0
    async def leave_match(self) -> None:
        if not self.match:
            if glob.config.debug:
                plog(f"{self} tried leaving a match they're not in?")
            return

        for s in self.match.slots:
            if self == s.player:
                s.reset()
                break

        await self.leave_channel(self.match.chat)

        if all(s.empty() for s in self.match.slots):
            # Multi is now empty, chat has been removed.
            # Remove the multi from the channels list.
            plog(f'Match {self.match} finished.')
            await glob.matches.remove(self.match)

            if lobby := glob.channels['#lobby']:
                lobby.enqueue(await packets.disposeMatch(self.match.id))
Пример #17
0
    async def join_channel(self, c: Channel) -> bool:
        if self in c:
            # User already in the channel.
            if glob.config.debug:
                plog(f'{self} was double-added to {c}.')

            return False

        if not self.priv & c.read:
            plog(f'{self} tried to join {c} but lacks privs.')
            return False

        # Lobby can only be interacted with while in mp lobby.
        if c._name == '#lobby' and not self.in_lobby:
            return False

        c.append(self)  # Add to channels
        self.channels.append(c)  # Add to player

        self.enqueue(await packets.channelJoin(c.name))

        # Update channel usercounts for all clients that can see.
        # For instanced channels, enqueue update to only players
        # in the instance; for normal channels, enqueue to all.
        targets = c.players if c.instance else glob.players

        for p in targets:
            p.enqueue(await packets.channelInfo(*c.basic_info))

        if glob.config.debug:
            plog(f'{self} joined {c}.')

        return True
Пример #18
0
    async def remove_spectator(self, p) -> None:
        self.spectators.remove(p)
        p.spectating = None

        c = glob.channels[f'#spec_{self.id}']
        await p.leave_channel(c)

        if not self.spectators:
            # Remove host from channel, deleting it.
            await self.leave_channel(c)
        else:
            fellow = await packets.fellowSpectatorLeft(p.id)
            c_info = await packets.channelInfo(*c.basic_info
                                               )  # new playercount

            self.enqueue(c_info)

            for s in self.spectators:
                s.enqueue(fellow + c_info)

        self.enqueue(await packets.spectatorLeft(p.id))
        plog(f'{p} is no longer spectating {self}.')
Пример #19
0
    async def save_to_sql(self) -> None:
        """Save the the object into sql."""
        if any(x is None
               for x in (self.md5, self.id, self.set_id, self.status,
                         self.artist, self.title, self.version, self.creator,
                         self.last_update, self.frozen, self.mode, self.bpm,
                         self.cs, self.od, self.ar, self.hp, self.diff)):
            plog('Tried to save invalid beatmap to SQL!', Ansi.LRED)
            return

        await glob.db.execute(
            'REPLACE INTO maps (id, set_id, status, md5, '
            'artist, title, version, creator, last_update, '
            'frozen, mode, bpm, cs, od, ar, hp, diff) VALUES ('
            '%s, %s, %s, %s, %s, %s, %s, %s, %s, '
            '%s, %s, %s, %s, %s, %s, %s, %s)', [
                self.id, self.set_id,
                int(self.status), self.md5, self.artist, self.title,
                self.version, self.creator, self.last_update, self.frozen,
                int(self.mode), self.bpm, self.cs, self.od, self.ar, self.hp,
                self.diff
            ])
Пример #20
0
    async def join_match(self, m: Match, passwd: str) -> bool:
        if self.match:
            plog(f'{self} tried to join multiple matches?')
            self.enqueue(await packets.matchJoinFail())
            return False

        if m.chat:  # Match already exists, we're simply joining.
            if passwd != m.passwd:  # eff: could add to if? or self.create_m..
                plog(f'{self} tried to join {m} with incorrect passwd.')
                self.enqueue(await packets.matchJoinFail())
                return False
            if (slotID := m.get_free()) is None:
                plog(f'{self} tried to join a full match.')
                self.enqueue(await packets.matchJoinFail())
                return False
Пример #21
0
    async def remove(self, p: Player) -> None:
        self.players.remove(p)

        if glob.config.debug:
            plog(f'{p} removed from players list.')
Пример #22
0
 async def dequeue(self) -> Optional[bytes]:
     """Get data from the queue to send to the client."""
     try:
         return self._queue.get_nowait()
     except:
         plog('Empty queue?')
Пример #23
0
    if 'ss' not in conn.files:
        plog(f'screenshot req missing file.', Ansi.LRED)
        return

    pname = unquote(conn.multipart_args['u'])
    phash = conn.multipart_args['p']

    if not (p := await glob.players.get_login(pname, phash)):
        return

    filename = f'{rstring(8)}.png'

    async with aiofiles.open(f'.data/ss/{filename}', 'wb') as f:
        await f.write(conn.files['ss'])

    plog(f'{p} uploaded {filename}.')
    return filename.encode()


@web_handler('osu-getfriends.php', required_args=('u', 'h'))
async def osuGetFriends(conn: AsyncConnection) -> Optional[bytes]:
    pname = unquote(conn.args['u'])
    phash = conn.args['h']

    if not (p := await glob.players.get_login(pname, phash)):
        return

    return '\n'.join(str(i) for i in p.friends).encode()


@web_handler('osu-getbeatmapinfo.php', required_args=('u', 'h'))
Пример #24
0
    async def add(self, m: Match) -> None:
        if m in self.matches:
            plog(f'{m} double-added to matches list?')
            return

        if (free := self.get_free()) is not None:
            # set the id of the match
            # to our free slot found.
            m.id = free
            self.matches[free] = m

            if glob.config.debug:
                plog(f'{m} added to matches list.')
        else:
            plog(f'Match list is full! Could not add {m}.')

    async def remove(self, m: Match) -> None:
        for idx, i in enumerate(self.matches):
            if m == i:
                self.matches[idx] = None
                break

        if glob.config.debug:
            plog(f'{m} removed from matches list.')


class PlayerList(Sequence):
    """A class to represent all players online on the gulag.

    Attributes
Пример #25
0
async def updateBeatmap(conn: AsyncConnection) -> Optional[bytes]:
    if not (re := regexes.mapfile.match(unquote(conn.path[10:]))):
        plog(f'Requested invalid map update {conn.path}.', Ansi.LRED)
        return
Пример #26
0
class Score:
    """A class to represent an osu! score.

    Attributes
    -----------
    id: `int`
        The score's unique ID.

    bmap: Optional[`Beatmap`]
        A beatmap obj representing the osu map.

    player: Optional[`Player`]
        A player obj of the player who submitted the score.

    pp: `float`
        The score's performance points.

    score: `int`
        The score's osu! score value.

    max_combo: `int`
        The maximum combo reached in the score.

    mods: `Mods`
        A bitwise value of the osu! mods used in the score.

    acc: `float`
        The accuracy of the score.

    n300: `int`
        The number of 300s in the score.

    n100: `int`
        The number of 100s in the score (150s if taiko).

    n50: `int`
        The number of 50s in the score.

    nmiss: `int`
        The number of misses in the score.

    ngeki: `int`
        The number of gekis in the score.

    nkatu: `int`
        The number of katus in the score.

    grade: `str`
        The letter grade in the score.

    rank: `int`
        The leaderboard placement of the score.

    passed: `bool`
        Whether the score completed the map.

    perfect: `bool`
        Whether the score is a full-combo.

    status: `SubmissionStatus`
        The submission status of the score.

    mode: `GameMode`
        The game mode of the score.

    play_time: `int`
        A UNIX timestamp of the time of score submission.

    time_elapsed: `int`
        The total elapsed time of the play (in milliseconds).

    client_flags: `int`
        osu!'s old anticheat flags.

    prev_best: Optional[`Score`]
        The previous best score before this play was submitted.
        NOTE: just because a score has a `prev_best` attribute does
        mean the score is our best score on the map! the `status`
        value will always be accurate for any score.
    """
    __slots__ = ('id', 'bmap', 'player', 'pp', 'score', 'max_combo', 'mods',
                 'acc', 'n300', 'n100', 'n50', 'nmiss', 'ngeki', 'nkatu',
                 'grade', 'rank', 'passed', 'perfect', 'status', 'mode',
                 'play_time', 'time_elapsed', 'client_flags', 'prev_best')

    def __init__(self):
        self.id = 0

        self.bmap: Optional[Beatmap] = None
        self.player: Optional[Player] = None

        self.pp = 0.0
        self.score = 0
        self.max_combo = 0
        self.mods = Mods.NOMOD

        self.acc = 0.0
        # TODO: perhaps abstract these differently
        # since they're mode dependant? feels weird..
        self.n300 = 0
        self.n100 = 0  # n150 for taiko
        self.n50 = 0
        self.nmiss = 0
        self.ngeki = 0
        self.nkatu = 0
        self.grade = Rank.F

        self.rank = 0
        self.passed = False
        self.perfect = False
        self.status = SubmissionStatus.FAILED

        self.mode = GameMode.vn_std
        self.play_time = 0
        self.time_elapsed = 0

        # osu!'s client 'anticheat'.
        self.client_flags = ClientFlags.Clean

        self.prev_best = None

    @classmethod
    async def from_sql(cls, scoreid: int, sql_table: str):
        """Create a score object from sql using it's scoreid."""
        # XXX: perhaps in the future this should take a gamemode rather
        # than just the sql table? just faster on the current setup :P
        res = await glob.db.fetch(
            'SELECT id, map_md5, userid, pp, score, '
            'max_combo, mods, acc, n300, n100, n50, '
            'nmiss, ngeki, nkatu, grade, perfect, '
            'status, mode, play_time, '
            'time_elapsed, client_flags '
            f'FROM {sql_table} WHERE id = %s', [scoreid],
            _dict=False)

        if not res:
            return

        s = cls()

        s.id = res[0]
        s.bmap = await Beatmap.from_md5(res[1])
        s.player = await glob.players.get_by_id(res[2], sql=True)

        (s.pp, s.score, s.max_combo, s.mods, s.acc, s.n300, s.n100, s.n50,
         s.nmiss, s.ngeki, s.nkatu, s.grade, s.perfect, s.status, mode_vn,
         s.play_time, s.time_elapsed, s.client_flags) = res[3:]

        # fix some types
        s.passed = s.status != 0
        s.status = SubmissionStatus(s.status)
        s.mods = Mods(s.mods)
        s.mode = GameMode.from_params(mode_vn, s.mods)
        s.client_flags = ClientFlags(s.client_flags)

        if s.bmap:
            s.rank = await s.calc_lb_placement()

        return s

    @classmethod
    async def from_submission(cls, data_enc: str, iv: str, osu_ver: str,
                              phash: str) -> None:
        """Create a score object from an osu! submission string."""
        cbc = RijndaelCbc(f'osu!-scoreburgr---------{osu_ver}',
                          iv=base64.b64decode(iv).decode('latin_1'),
                          padding=ZeroPadding(32),
                          block_size=32)

        data = cbc.decrypt(
            base64.b64decode(data_enc).decode('latin_1')).decode().split(':')

        if len(data) != 18:
            plog('Received an invalid score submission.', Ansi.LRED)
            return

        s = cls()

        if len(map_md5 := data[0]) != 32:
            return

        pname = data[1].rstrip()  # why does osu! make me rstrip lol

        # Get the map & player for the score.
        s.bmap = await Beatmap.from_md5(map_md5)
        s.player = await glob.players.get_login(pname, phash)

        if not s.player:
            # Return the obj with an empty player to
            # determine whether the score faield to
            # be parsed vs. the user could not be found
            # logged in (we want to not send a reply to
            # the osu! client if they're simply not logged
            # in, so that it will retry once they login).
            return s

        # XXX: unused idx 2: online score checksum
        # Perhaps will use to improve security at some point?

        # Ensure all ints are safe to cast.
        if not all(i.isdecimal() for i in data[3:11] + [data[13], data[15]]):
            plog('Invalid parameter passed into submit-modular.', Ansi.LRED)
            return

        (s.n300, s.n100, s.n50, s.ngeki, s.nkatu, s.nmiss, s.score,
         s.max_combo) = (int(i) for i in data[3:11])

        s.perfect = data[11] == '1'
        _grade = data[12]  # letter grade
        s.mods = Mods(int(data[13]))
        s.passed = data[14] == 'True'
        s.mode = GameMode.from_params(int(data[15]), s.mods)
        s.play_time = int(time.time())  # (yyMMddHHmmss)
        s.client_flags = data[17].count(' ')  # TODO: use osu!ver? (osuver\s+)

        s.grade = _grade if s.passed else 'F'

        # All data read from submission.
        # Now we can calculate things based on our data.
        s.calc_accuracy()

        if s.bmap:
            # Ignore SR for now.
            s.pp = (await s.calc_diff())[0]

            await s.calc_status()
            s.rank = await s.calc_lb_placement()
        else:
            s.pp = 0.0
            s.status = SubmissionStatus.SUBMITTED if s.passed \
                  else SubmissionStatus.FAILED

        return s
Пример #27
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:
        plog('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(await 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:
        plog(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 = autorestrict_pp[s.mode][s.mods & Mods.FLASHLIGHT != 0]

        if s.pp > pp_cap:
            plog(
                f'{s.player} restricted for submitting '
                f'{s.pp:.2f} score on gm {s.mode!r}.', Ansi.LRED)

            await s.player.restrict()
            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':
            plog(f'{s.player} submitted a score without a replay!', Ansi.LRED)
            await s.player.restrict()
        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)
Пример #28
0
            slotID = 0
            await glob.matches.add(m)  # add to global matchlist
            # This will generate an ID.

            await glob.channels.add(
                Channel(name=f'#multi_{m.id}',
                        topic=f"MID {m.id}'s multiplayer channel.",
                        read=Privileges.Normal,
                        write=Privileges.Normal,
                        auto_join=False,
                        instance=True))

            m.chat = glob.channels[f'#multi_{m.id}']

        if not await self.join_channel(m.chat):
            plog(f'{self} failed to join {m.chat}.')
            return False

        if (lobby := glob.channels['#lobby']) in self.channels:
            await self.leave_channel(lobby)

        slot = m.slots[0 if slotID == -1 else slotID]

        slot.status = SlotStatus.not_ready
        slot.player = self
        self.match = m
        self.enqueue(await packets.matchJoinSuccess(m))
        m.enqueue(await packets.updateMatch(m))

        return True
Пример #29
0
    async def unrestrict(self) -> None:
        self.priv &= Privileges.Normal
        await glob.db.execute('UPDATE users SET priv = %s WHERE id = %s',
                              [int(self.priv), self.id])

        plog(f'Unrestricted {self}.', Ansi.CYAN)
Пример #30
0
    async def remove(self, c: Channel) -> None:
        self.channels.remove(c)

        if glob.config.debug:
            plog(f'{c} removed from channels list.')