Beispiel #1
0
    async def handle(self, p: Player) -> None:
        if not p.match:
            return

        if not (t := await glob.players.get(id=self.user_id)):
            log(f'{p} tried to invite a user who is not online! ({self.user_id})')
            return
Beispiel #2
0
async def handle_conn(conn: AsyncConnection) -> None:
    if 'Host' not in conn.headers:
        await conn.send(400, b'Missing required headers.')
        return

    st = time.time_ns()
    handler = None

    domain = conn.headers['Host']

    # match the host & uri to the correct handlers.

    if domain.endswith('.ppy.sh'):
        # osu! handlers
        subdomain = domain.removesuffix('.ppy.sh')

        if subdomain in ('c', 'ce', 'c4', 'c5', 'c6'):
            # connection to `c[e4-6]?.ppy.sh/*`
            handler = handle_bancho
        elif subdomain == 'osu':
            # connection to `osu.ppy.sh/*`
            if conn.path.startswith('/web/'):
                handler = handle_web
            elif conn.path.startswith('/ss/'):
                handler = handle_ss
            elif conn.path.startswith('/d/'):
                handler = handle_dl
            elif conn.path.startswith('/api/'):
                handler = handle_api
            elif conn.path == '/users':
                handler = handle_registration
        elif subdomain == 'a':
            handler = handle_avatar

    else:
        # non osu!-related handler
        if domain.endswith(glob.config.domain):
            if conn.path.startswith('/api/'):
                handler = handle_api  # gulag!api
            else:
                # frontend handler?
                ...
        else:
            # nginx sending something that we're not handling?
            ...

    if handler:
        # we have a handler for this request.
        await handler(conn)
    else:
        # we have no such handler.
        log(f'Unhandled {conn.path}.', Ansi.LRED)
        await conn.send(400, b'Request handler not implemented.')

    if glob.config.debug:
        time_taken = (time.time_ns() - st) / 1000  # nanos -> micros
        time_str = (f'{time_taken:.2f}μs'
                    if time_taken < 1000 else f'{time_taken / 1000:.2f}ms')

        log(f'Request handled in {time_str}.', Ansi.LCYAN)
Beispiel #3
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:
                log('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
Beispiel #4
0
    async def __anext__(self):
        # do not break until we've read the
        # header of a packet we can handle.
        while True:
            p_type, p_len = await self.read_header()

            if p_type == ClientPacketType.PING:
                # the client is simply informing us that it's
                # still active; we don't have to handle anything.
                continue

            if p_type not in glob.bancho_map:
                # cannot handle - remove from
                # internal buffer and continue.
                log(f'Unhandled: {p_type!r}', Ansi.LYELLOW)

                if p_len != 0:
                    self._buf = self._buf[p_len:]
            else:
                # we can handle this one.
                break

        # we have a packet handler for this.
        self._current = glob.bancho_map[p_type]()
        self._current.length = p_len

        if self._current.args:
            await self.read_arguments()

        return self._current
Beispiel #5
0
    async def rm_donor(userid: int, delay: int):
        await asyncio.sleep(delay)

        p = await glob.players.get(id=userid, sql=True)
        await p.remove_privs(Privileges.Donator)

        log(f"{p}'s donation perks have expired.", Ansi.MAGENTA)
Beispiel #6
0
    async def maps_from_sql(self) -> None:
        """Retrieve all maps from sql to populate `self.maps`."""
        query = ('SELECT map_id, mods, slot '
                 'FROM tourney_pool_maps '
                 'WHERE pool_id = %s')

        for row in await glob.db.fetchall(query, [self.id]):
            map_id = row['map_id']
            bmap = await Beatmap.from_bid(map_id)

            if not bmap:
                # map not found? remove it from the
                # pool and log this incident to console.
                # NOTE: it's intentional that this removes
                # it from not only this pool, but all pools.
                # TODO: perhaps discord webhook?
                log(f'Removing {map_id} from pool {self.name} (not found).',
                    Ansi.LRED)

                await glob.db.execute(
                    'DELETE FROM tourney_pool_maps '
                    'WHERE map_id = %s', [map_id])
                continue

            key = (Mods(row['mods']), row['slot'])
            self.maps[key] = bmap
Beispiel #7
0
async def donor_expiry() -> list[Coroutine]:
    """Add new donation ranks & enqueue tasks to remove current ones."""
    # TODO: this system can get quite a bit better; rather than just
    # removing, it should rather update with the new perks (potentially
    # a different tier, enqueued after their current perks).

    async def rm_donor(userid: int, when: int):
        if (delta := when - time.time()) >= 0:
            await asyncio.sleep(delta)

        p = await glob.players.get_ensure(id=userid)

        # TODO: perhaps make a `revoke_donor` method?
        await p.remove_privs(Privileges.Donator)
        await glob.db.execute(
            'UPDATE users '
            'SET donor_end = 0 '
            'WHERE id = %s',
            [p.id]
        )

        if p.online:
            p.enqueue(packets.notification('Your supporter status has expired.'))

        log(f"{p}'s supporter status has expired.", Ansi.LMAGENTA)
Beispiel #8
0
    async def handle(self, p: Player) -> None:
        # TODO: match validation..?
        if p.silenced:
            p.enqueue(packets.matchJoinFail() + packets.notification(
                'Multiplayer is not available while silenced.'))
            return

        if not glob.matches.append(self.match):
            # failed to create match (match slots full).
            p.send('Failed to create match (no slots available).',
                   sender=glob.bot)
            p.enqueue(packets.matchJoinFail())
            return

        # create the channel and add it
        # to the global channel list as
        # an instanced channel.
        chan = Channel(name=f'#multi_{self.match.id}',
                       topic=f"MID {self.match.id}'s multiplayer channel.",
                       auto_join=False,
                       instance=True)

        glob.channels.append(chan)
        self.match.chat = chan

        await p.update_latest_activity()
        p.join_match(self.match, self.match.passwd)
        log(f'{p} created a new multiplayer match.')
Beispiel #9
0
def main() -> None:
    # Initialize pong.py
    init()

    # Log
    log("pong.py is now running!", Ansi.LGREEN)

    while True:
        # Draw Window
        draw(window)

        # Handle Events
        for event in pygame.event.get():
            # Key Up Event
            if event.type == KEYUP:
                keyup(event)
            # Key Down Event
            elif event.type == KEYDOWN:
                keydown(event)
            # Quit Event
            elif event.type == QUIT:
                log("Quitting pong.py! Thank you for playing!", Ansi.LRED)
                pygame.quit()
                sys.exit()

        # Update Display
        pygame.display.update()

        # Set FPS
        fps.tick(60)
Beispiel #10
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 #11
0
async def get_replay():
    id = request.args.get('id', type=int)
    mods = request.args.get('mods', type=str)

    # check if required parameters are met
    if not id:
        return b'missing parameters! (id)'

    if mods not in valid_mods:
        return b'invalid mods! (vn, rx, ap)'

    # fetch scores
    q = [
        'SELECT scores_{0}.*, maps.*, users.name FROM scores_{0}'.format(mods)
    ]

    args = []

    q.append(f'JOIN maps ON scores_{mods}.map_md5 = maps.md5')
    q.append(f'JOIN users ON scores_{mods}.userid = users.id')
    q.append(f'WHERE scores_{mods}.id = %s')
    args.append(id)

    if glob.config.debug:
        log(' '.join(q), Ansi.LGREEN)
    res = await glob.db.fetch(' '.join(q), args)
    return jsonify(res) if res else b'{}'
Beispiel #12
0
async def run_server(addr: Address) -> None:
    glob.version = Version(2, 8, 5)
    glob.http = aiohttp.ClientSession(json_serialize=orjson.dumps)

    loop = asyncio.get_event_loop()

    try:
        loop.add_signal_handler(signal.SIGINT, loop.stop)
        loop.add_signal_handler(signal.SIGTERM, loop.stop)
    except NotImplementedError:
        pass

    glob.db = AsyncSQLPoolWrapper()
    await glob.db.connect(**glob.config.mysql)

    # create our bot & append it to the global player list.
    glob.bot = Player(id=1, name='Aika', priv=Privileges.Normal)
    glob.bot.last_recv_time = 0x7fffffff

    glob.players.add(glob.bot)

    # add all channels from db.
    async for chan in glob.db.iterall('SELECT * FROM channels'):
        await glob.channels.add(Channel(**chan))

    # run background process to
    # disconnect inactive clients.
    loop.create_task(disconnect_inactive())

    async with AsyncTCPServer(addr) as glob.serv:
        log(f'Gulag v{glob.version} online!', AnsiRGB(0x00ff7f))
        async for conn in glob.serv.listen(glob.config.max_conns):
            loop.create_task(handle_conn(conn))
Beispiel #13
0
    def remove(self, m: 'Match') -> None:
        for i, _m in enumerate(self):
            if m is _m:
                self[i] = None
                break

        if glob.config.debug:
            log(f'{m} removed from matches list.')
Beispiel #14
0
    async def handle(self, p: Player) -> None:
        if (time.time() - p.login_time) < 2:
            # osu! has a weird tendency to log out immediately when
            # it logs in, then reconnects? not sure why..?
            return

        await p.logout()
        log(f'{p} logged out.', Ansi.LYELLOW)
Beispiel #15
0
    async def handle(self, p: Player) -> None:
        host = p.spectating

        if not host:
            log(f"{p} tried to stop spectating when they're not..?", Ansi.LRED)
            return

        await host.remove_spectator(p)
Beispiel #16
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:
            log(f'{m} removed from matches list.')
Beispiel #17
0
    async def handle(self, user):
        if (time.time() - user['ltime']) < 1:
            return # osu sends random logout packet token on login ?

        ucache = glob.cache['user']
        del ucache[user['token']]
        glob.players.remove(user)
        enqueue(packets.logout(user['id']))
        log(f"{user['name']} logged out.", Ansi.LBLUE)
Beispiel #18
0
    async def handle(self, p: Player) -> None:
        c = glob.channels[self.name]

        if not c or not await p.join_channel(c):
            log(f'{p} failed to join {self.name}.', Ansi.YELLOW)
            return

        # enqueue channelJoin to our player.
        p.enqueue(packets.channelJoin(c.name))
Beispiel #19
0
    async def handle(self, p: Player) -> None:
        if not 0 <= self.match_id < 64:
            # make sure it's
            # a valid match id.
            return

        if not (m := glob.matches[self.match_id]):
            log(f'{p} tried to join a non-existant mp lobby?')
            return
Beispiel #20
0
    def remove(self, m: 'Match') -> None:
        """Remove `m` from the list."""
        for i, _m in enumerate(self):
            if m is _m:
                self[i] = None
                break

        if glob.app.debug:
            log(f'{m} removed from matches list.')
Beispiel #21
0
    async def add(self, c: Channel) -> None:
        if c in self.channels:
            log(f'{c} double-added to channels list?')
            return

        self.channels.append(c)

        if glob.config.debug:
            log(f'{c} added to channels list.')
Beispiel #22
0
    async def prepare(cls) -> None:
        """Fetch data from sql & return; preparing to run the server."""
        log('Fetching clans from sql', Ansi.LCYAN)
        res = await glob.db.fetchall('SELECT * FROM clans')
        obj = cls([Clan(**row) for row in res])

        for clan in obj:
            await clan.members_from_sql()

        return obj
Beispiel #23
0
    def add(self, p: Player) -> None:
        if p in self.players:
            if glob.config.debug:
                log(f'{p} double-added to global player list?')
            return

        self.players.append(p)

        if glob.config.debug:
            log(f'{p} added to global player list.')
Beispiel #24
0
    async def _update_cmyui(self) -> None:
        """Check if cmyui_pkg has a newer release; update if available."""
        module_ver = Version.from_str(pkg_version('cmyui'))
        latest_ver = await self._get_latest_cmyui()

        if module_ver < latest_ver:
            # package is not up to date; update it.
            log(f'Updating cmyui_pkg (v{module_ver!r} -> '
                                    f'v{latest_ver!r}).', Ansi.MAGENTA)
            pip_main(['install', '-Uq', 'cmyui']) # Update quiet
Beispiel #25
0
 async def prepare(cls) -> None:
     """Fetch data from sql & return; preparing to run the server."""
     log('Fetching channels from sql', Ansi.LCYAN)
     return cls(
         Channel(name=row['name'],
                 topic=row['topic'],
                 read_priv=Privileges(row['read_priv']),
                 write_priv=Privileges(row['write_priv']),
                 auto_join=row['auto_join'] == 1)
         for row in await glob.db.fetchall('SELECT * FROM channels'))
Beispiel #26
0
    async def add_friend(self, p: 'Player') -> None:
        if p.id in self.friends:
            log(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])

        log(f'{self} added {p} to their friends.')
Beispiel #27
0
    async def add(self, c: Channel) -> None:
        """Attempt to add `c` to the list."""
        if c in self.channels:
            log(f'{c} double-added to channels list?')
            return

        self.channels.append(c)

        if glob.config.debug:
            log(f'{c} added to channels list.')
Beispiel #28
0
async def bancho_handler(conn: Connection) -> bytes:
    if ('User-Agent' not in conn.headers
            or conn.headers['User-Agent'] != 'osu!'):
        return

    # check for 'osu-token' in the headers.
    # if it's not there, this is a login request.

    if 'osu-token' not in conn.headers:
        # login is a bit of a special case,
        # so we'll handle it separately.
        async with glob.players._lock:
            resp, token = await login(conn.body, conn.headers['X-Real-IP'],
                                      conn.headers)

        conn.add_resp_header(f'cho-token: {token}')
        return resp

    # get the player from the specified osu token.
    player = await glob.players.get(token=conn.headers['osu-token'])

    if not player:
        # token was not found; changes are, we just restarted
        # the server. just tell their client to re-connect.
        return packets.restartServer(0)

    # bancho connections can be comprised of multiple packets;
    # our reader is designed to iterate through them individually,
    # allowing logic to be implemented around the actual handler.

    # NOTE: the reader will internally discard any
    # packets whose logic has not been defined.
    # TODO: why is the packet reader async lol
    async for packet in BanchoPacketReader(conn.body):
        await packet.handle(player)

        if glob.config.debug:
            log(f'{packet.type!r}', Ansi.LMAGENTA)

    player.last_recv_time = time.time()

    # TODO: this could probably be done better?
    resp = bytearray()

    while not player.queue_empty():
        # read all queued packets into stream
        resp += player.dequeue()

    conn.add_resp_header('Content-Type: text/html; charset=UTF-8')
    resp = bytes(resp)

    # even if the packet is empty, we have to
    # send back an empty response so the client
    # knows it was successfully delivered.
    return resp
Beispiel #29
0
    def append(self, p: Player) -> None:
        """Attempt to add `p` to the list."""
        if p in self.players:
            if glob.config.debug:
                log(f'{p} double-added to global player list?')
            return

        self.players.append(p)

        if glob.config.debug:
            log(f'{p} added to global player list.')
Beispiel #30
0
    def append(self, p: Player) -> None:
        """Append `p` to the list."""
        if p in self:
            if glob.config.debug:
                log(f'{p} double-added to global player list?')
            return

        super().append(p)

        if glob.config.debug:
            log(f'{p} added to global player list.')