Beispiel #1
0
def getScores(req: Request) -> Optional[bytes]:
    if not all(x in req.args for x in required_params_getScores):
        printlog(f'get-scores req missing params.')
        return

    # Tbh, I don't really care if people request
    # leaderboards from other peoples accounts, or are
    # not logged out.. At the moment, there are no checks
    # that could put anyone's account in danger :P.
    # XXX: could be a ddos problem? lol

    if len(req.args['c']) != 32 \
    or not req.args['mods'].isnumeric():
        return b'-1|false'

    req.args['mods'] = int(req.args['mods'])

    res: List[bytes] = []

    if req.args['mods'] & Mods.RELAX:
        table = 'scores_rx'
        scoring = 'pp'
    else:
        table = 'scores_vn'
        scoring = 'score'

    if not (bmap := Beatmap.from_md5(req.args['c'])):
        # Couldn't find in db or at osu! api.
        # The beatmap must not be submitted.
        return b'-1|false'
Beispiel #2
0
def osuSearchHandler(req: Request) -> Optional[bytes]:
    if not all(x in req.args for x in required_params_osuSearch):
        printlog(f'osu-search req missing params.')
        return

    if not (p := glob.players.get_from_cred(req.args['u'], req.args['h'])):
        return
Beispiel #3
0
def updateBeatmap(req: Request) -> Optional[bytes]:
    # XXX: This currently works in updating the map, but
    # seems to get the checksum something like that wrong?
    # Will have to look into it :P
    if not (re := _map_regex.match(unquote(req.uri[10:]))):
        printlog(f'Requested invalid map update {req.uri}.', Ansi.RED)
        return b''
Beispiel #4
0
def lastFM(req: Request) -> Optional[bytes]:
    if not all(x in req.args for x in required_params_lastFM):
        printlog(f'lastfm req missing params.')
        return

    if not (p := glob.players.get_from_cred(req.args['us'], req.args['ha'])):
        return
Beispiel #5
0
    def add(self, m: Match) -> bool:
        if m in self.matches:
            printlog(f'{m} already in matches list!')
            return False

        if (free := self.get_free()) is None:
            printlog(f'Match list is full! Could not add {m}.')
            return False
Beispiel #6
0
    def leave_channel(self, c: Channel) -> None:
        if self not in c:
            printlog(f'{self} tried to leave {c} but is not in it.')
            return

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

        self.enqueue(packets.channelKick(c.name))
        printlog(f'{self} left {c}.')
Beispiel #7
0
    def add_friend(self, p) -> None:
        if p.id in self.friends:
            printlog(f'{self} tried to add {p}, who is already their friend!')
            return

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

        printlog(f'{self} added {p} to their friends.')
Beispiel #8
0
    def remove_friend(self, p) -> None:
        if not p.id in self.friends:
            printlog(f'{self} tried to remove {p}, who is not their friend!')
            return

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

        printlog(f'{self} removed {p} from their friends.')
Beispiel #9
0
    def read_packet_header(self) -> None:
        if len(self.data) < 7:
            # packet is invalid, end connection
            self.packetID = -1
            self._offset += len(self.data)
            printlog(f'[ERR] Data misread! (len: {len(self.data)})',
                     Ansi.LIGHT_RED)
            return

        self.packetID, self.length = struct.unpack('<HxI', self.data[:7])
        self._offset += 7  # Read our first 7 bytes for packetid & len
Beispiel #10
0
def osuScreenshot(req: Request) -> Optional[bytes]:
    if not all(x in req.args for x in required_params_screemshot):
        printlog(f'screenshot req missing params.')
        return

    if 'ss' not in req.files:
        printlog(f'screenshot req missing file.')
        return

    if not (p := glob.players.get_from_cred(req.args['u'], req.args['p'])):
        return
Beispiel #11
0
def getReplay(req: Request) -> Optional[bytes]:
    if not all(x in req.args for x in required_params_getReplay):
        printlog(f'get-scores req missing params.')
        return

    path = f"replays/{req.args['c']}.osr"
    if not exists(path):
        return b''

    with open(path, 'rb') as f:
        data = f.read()

    return data
Beispiel #12
0
    def restrict(self) -> None:  # TODO: reason
        self.priv &= ~Privileges.Normal
        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(packets.userID(-1))
            self.enqueue(
                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.'))

        printlog(f'Restricted {self}.', Ansi.CYAN)
Beispiel #13
0
    def leave_match(self) -> None:
        if not self.match:
            printlog(f'{self} tried leaving a match but is not in one?')
            return

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

        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.
            printlog(f'Match {self.match} finished.')
            glob.matches.remove(self.match)

            if (lobby := glob.channels.get('#lobby')):
                lobby.enqueue(packets.disposeMatch(self.match.id))
Beispiel #14
0
    def from_submission(cls, data_enc: str, iv: str,
                        osu_ver: str, pass_md5: str) -> None:
        """Create a score object from an osu! submission string."""
        aes_key = f'osu!-scoreburgr---------{osu_ver}'
        cbc = RijndaelCbc(
            f'osu!-scoreburgr---------{osu_ver}',
            iv = b64decode(iv).decode('latin_1'),
            padding = ZeroPadding(32), block_size =  32
        )

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

        if len(data) != 18:
            printlog('Received an invalid score submission.', Ansi.LIGHT_RED)
            return None

        s = cls()

        if len(map_md5 := data[0]) != 32:
            return
Beispiel #15
0
    def remove_spectator(self, p) -> None:
        self.spectators.remove(p)
        p.spectating = None

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

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

            self.enqueue(c_info)

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

        self.enqueue(packets.spectatorLeft(p.id))
        printlog(f'{p} is no longer spectating {self}.')
Beispiel #16
0
    def save_to_sql(self) -> None:
        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)):
            printlog('Tried to save invalid beatmap to SQL!', Ansi.LIGHT_RED)
            return

        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
            ])
Beispiel #17
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 #18
0
class MatchList(Sequence):
    """A class to represent all multiplayer matches on the gulag.

    Attributes
    -----------
    matches: List[Optional[:class:`Match`]]
        A list of match objects representing the current mp matches.
        The size of this attr is constant; slots will be None if not in use.
    """
    __slots__ = ('matches', )

    def __init__(self):
        self.matches = [None for _ in range(32)]  # Max matches.

    def __getitem__(self, index: Slice) -> Optional[Match]:
        return self.matches[index]

    def __len__(self) -> int:
        return len(self.matches)

    def __contains__(self, m: Match) -> bool:
        return m in self.matches

    def get_free(self) -> Optional[Match]:
        # Return first free match.
        for idx, m in enumerate(self.matches):
            if not m: return idx

    def get_by_id(self, mid: int) -> Optional[Match]:
        for m in self.matches:
            if m and m.id == mid:
                return m

    def add(self, m: Match) -> bool:
        if m in self.matches:
            printlog(f'{m} already in matches list!')
            return False

        if (free := self.get_free()) is None:
            printlog(f'Match list is full! Could not add {m}.')
            return False

        m.id = free
        printlog(f'Adding {m} to matches list.')
        self.matches[free] = m
Beispiel #19
0
    def join_match(self, m: Match, passwd: str) -> bool:
        if self.match:
            printlog(f'{self} tried to join multiple matches?')
            self.enqueue(packets.matchJoinFail(m))
            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..
                printlog(f'{self} tried to join {m} with incorrect passwd.')
                self.enqueue(packets.matchJoinFail(m))
                return False
            if (slotID := m.get_free()) is None:
                printlog(f'{self} tried to join a full match.')
                self.enqueue(packets.matchJoinFail(m))
                return False
Beispiel #20
0
    def join_channel(self, c: Channel) -> bool:
        if self in c:
            printlog(f'{self} tried to double join {c}.')
            return False

        if not self.priv & c.read:
            printlog(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(packets.channelJoin(c.name))
        printlog(f'{self} joined {c}.')
        return True
Beispiel #21
0
 def dequeue(self) -> bytes:
     try:
         return self._queue.get_nowait()
     except:
         printlog('Empty queue?')
Beispiel #22
0
        return cls(**res)

    @classmethod
    def from_md5(cls, md5: str, cache_pp: bool = False):
        # Try to get from sql.
        if (m := cls.from_md5_sql(md5)):
            if cache_pp:
                m.cache_pp()

            return m

        # Not in sql, get from osu!api.
        if glob.config.osu_api_key:
            return cls.from_md5_osuapi(md5)

        printlog('Fetching beatmap requires osu!api key.', Ansi.LIGHT_RED)

    @classmethod
    def from_md5_sql(cls, md5: str):
        if not (res := glob.db.fetch(
                'SELECT id, set_id, status, '
                'artist, title, version '
                'FROM maps WHERE md5 = %s', [md5])):
            return

        res['md5'] = md5
        return cls(**res)

    @classmethod
    def from_md5_osuapi(cls, md5: str):
        if not (r := req_get(
Beispiel #23
0
    def unrestrict(self) -> None:
        self.priv &= Privileges.Normal
        glob.db.execute('UPDATE users SET priv = %s WHERE id = %s',
                        [int(self.priv), self.id])

        printlog(f'Unrestricted {self}.', Ansi.CYAN)
Beispiel #24
0
def submitModularSelector(req: Request) -> Optional[bytes]:
    if not all(x in req.args for x in required_params_submitModular):
        printlog(f'submit-modular-selector req missing params.')
        return b'error: no'

    # TODO: make some kind of beatmap object.
    # We currently don't check the map's ranked status.

    # Parse our score data into a score obj.
    s: Score = Score.from_submission(req.args['score'], req.args['iv'],
                                     req.args['osuver'], req.args['pass'])

    if not s:
        printlog('Failed to parse a score - invalid format.', Ansi.YELLOW)
        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.

    table = 'scores_rx' if s.mods & Mods.RELAX else 'scores_vn'

    # Check for score duplicates
    # TODO: might need to improve?
    res = glob.db.fetch(
        f'SELECT 1 FROM {table} WHERE game_mode = %s '
        'AND map_md5 = %s AND userid = %s AND mods = %s '
        'AND score = %s',
        [s.game_mode, s.bmap.md5, s.player.id, s.mods, s.score])

    if res:
        printlog(f'{s.player} submitted a duplicate score.', Ansi.LIGHT_YELLOW)
        return b'error: no'

    if req.args['i']:
        breakpoint()

    gm = GameMode(s.game_mode + (4 if s.player.rx and s.game_mode != 3 else 0))

    if not s.player.priv & Privileges.Whitelisted:
        # Get the PP cap for the current context.
        pp_cap = autorestrict_pp[gm][s.mods & Mods.FLASHLIGHT != 0]

        if s.pp > pp_cap:
            printlog(
                f'{s.player} restricted for submitting {s.pp:.2f} score on gm {s.game_mode}.',
                Ansi.LIGHT_RED)
            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.
        glob.db.execute(
            f'UPDATE {table} SET status = 1 '
            'WHERE status = 2 and map_md5 = %s '
            'AND userid = %s', [s.bmap.md5, s.player.id])

    s.id = 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.bmap.md5, s.score, s.pp, s.acc, s.max_combo, s.mods, s.n300,
            s.n100, s.n50, s.nmiss, s.ngeki, s.nkatu,
            int(s.status), s.game_mode, s.play_time, 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 req.files or req.files['score'] == b'\r\n':
            printlog(f'{s.player} submitted a score without a replay!',
                     Ansi.LIGHT_RED)
            s.player.restrict()
        else:
            # Save our replay
            with open(f'replays/{s.id}.osr', 'wb') as f:
                f.write(req.files['score'])

    s.player.stats[gm].tscore += s.score
    if s.bmap.status in {RankedStatus.Ranked, RankedStatus.Approved}:
        s.player.stats[gm].rscore += s.score

    glob.db.execute(
        'UPDATE stats SET rscore_{0:sql} = %s, '
        'tscore_{0:sql} = %s WHERE id = %s'.format(gm),
        [s.player.stats[gm].rscore, s.player.stats[gm].tscore, s.player.id])

    if s.status == SubmissionStatus.BEST and s.rank == 1:
        # Announce the user's #1 score.
        if announce_chan := glob.channels.get('#announce'):
            announce_chan.send(
                glob.bot, f'{s.player.embed} achieved #1 on {s.bmap.embed}.')
Beispiel #25
0
        printlog(f'screenshot req missing params.')
        return

    if 'ss' not in req.files:
        printlog(f'screenshot req missing file.')
        return

    if not (p := glob.players.get_from_cred(req.args['u'], req.args['p'])):
        return

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

    with open(f'screenshots/{filename}', 'wb+') as f:
        f.write(req.files['ss'])

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


required_params_lastFM = frozenset({'b', 'action', 'us', 'ha'})


@web_handler('lastfm.php')
def lastFM(req: Request) -> Optional[bytes]:
    if not all(x in req.args for x in required_params_lastFM):
        printlog(f'lastfm req missing params.')
        return

    if not (p := glob.players.get_from_cred(req.args['us'], req.args['ha'])):
        return
Beispiel #26
0
 def fetch_geoloc(self, ip: str) -> None:
     if not (res := req_get(f'http://ip-api.com/json/{ip}')):
         printlog('Failed to get geoloc data: request failed.',
                  Ansi.LIGHT_RED)
         return
Beispiel #27
0
 def remove(self, c: Channel) -> None:
     printlog(f'Removing {c} from channels list.')
     self.channels.remove(c)
Beispiel #28
0
class Score:
    """A class to represent a score.

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

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

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

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

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

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

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

    acc: :class:`float`
        The accuracy of the score.

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

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

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

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

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

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

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

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

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

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

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

    game_mode: :class:`int`
        The game mode of the score.

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

    client_flags: :class:`int`
        osu!'s old anticheat flags.
    """
    __slots__ = (
        'id', 'map', 'player',
        'pp', 'score', 'max_combo', 'mods',
        'acc', 'n300', 'n100', 'n50', 'nmiss', 'ngeki', 'nkatu', 'grade',
        'rank', 'passed', 'perfect', 'status',
        'game_mode', 'play_time',
        'client_flags'
    )

    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.game_mode = 0
        self.play_time = 0

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

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

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

        if len(data) != 18:
            printlog('Received an invalid score submission.', Ansi.LIGHT_RED)
            return None

        s = cls()

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

        s.bmap = Beatmap.from_md5(map_md5)

        # why does osu! make me rstrip lol
        s.player = glob.players.get_from_cred(data[1].rstrip(), pass_md5)
        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.isnumeric() for i in data[3:11] + [data[13] + data[15]]):
            printlog('Invalid parameter passed into submit-modular.', Ansi.LIGHT_RED)
            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'
        s.grade = data[12] # letter grade
        s.mods = int(data[13])
        s.passed = data[14] == 'True'
        s.game_mode = int(data[15])
        s.play_time = int(time()) # (yyMMddHHmmss)
        s.client_flags = data[17].count(' ') # TODO: use osu!ver? (osuver\s+)

        # 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 = s.calc_diff()[0]
            s.calc_status()
            s.rank = s.calc_lb_placement()
        else:
            s.pp = 0.0
            s.status = SubmissionStatus.SUBMITTED if s.passed \
                  else SubmissionStatus.FAILED

        return s
Beispiel #29
0
 def add(self, c: Channel) -> None:  # bool ret success?
     if c in self.channels:
         printlog(f'{c} already in channels list!')
         return
     printlog(f'Adding {c} to channels list.')
     self.channels.append(c)
Beispiel #30
0
            slotID = 0
            glob.matches.add(m)  # add to global matchlist
            # This will generate an ID.

            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,
                        temp=True))

            m.chat = glob.channels.get(f'#multi_{m.id}')

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

        if (lobby := glob.channels.get('#lobby')) in self.channels:
            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(packets.matchJoinSuccess(m))
        m.enqueue(packets.updateMatch(m))

        return True