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
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)
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
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
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)
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
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)
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.')
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)
async def getScores(p: Player, conn: AsyncConnection) -> Optional[bytes]: isdecimal_n = partial(_isdecimal, _negative=True) # make sure all int args are integral if not all(isdecimal_n(conn.args[k]) for k in ('mods', 'v', 'm', 'i')): return b'-1|false' map_md5 = conn.args['c'] mods = int(conn.args['mods']) mode = GameMode.from_params(int(conn.args['m']), mods) map_set_id = int(conn.args['i']) rank_type = RankingType(int(conn.args['v'])) # attempt to update their stats if their # gm/gm-affecting-mods change at all. if mode != p.status.mode: p.status.mods = mods p.status.mode = mode glob.players.enqueue(packets.userStats(p)) table = mode.sql_table scoring = 'pp' if mode >= GameMode.rx_std else 'score' if not (bmap := await Beatmap.from_md5(map_md5, map_set_id)): # couldn't find in db or at osu! api by md5. # check if we have the map in our db (by filename). filename = conn.args['f'].replace('+', ' ') if not (re := regexes.mapfile.match(unquote(filename))): log(f'Requested invalid file - {filename}.', Ansi.LRED) return
async def 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'{}'
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))
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.')
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)
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)
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.')
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)
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))
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
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.')
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.')
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
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.')
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
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'))
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.')
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.')
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
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.')
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.')