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))
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.')
async def switch_server(p: Player, c: Messageable, msg: Sequence[str]) -> str: """Switch servers to a specified ip address.""" if len(msg) != 1: return 'Invalid syntax: !switch <endpoint>' p.enqueue(packets.switchTournamentServer(msg[0])) return 'Have a nice journey..'
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 channelJoin(p: Player, pr: BanchoPacketReader) -> None: chan_name, = await pr.read(osuTypes.string) c = glob.channels[chan_name] if not c or not await p.join_channel(c): plog(f'{p} failed to join {chan_name}.', Ansi.YELLOW) return # Enqueue channelJoin to our player. p.enqueue(await packets.channelJoin(c.name))
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))
async def statsRequest(p: Player, pr: BanchoPacketReader) -> None: if len(pr.data) < 6: return userIDs = await 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): if t := await glob.players.get_by_id(online): p.enqueue(await packets.userStats(t))
async def handle(self, p: Player) -> None: # XXX: this only sends when the client can see > 256 players, # so this probably won't have much use for private servers. # NOTE: i'm not exactly sure how bancho implements this and whether # i'm supposed to filter the users presences to send back with the # player's presence filter; i can add it in the future perhaps. for t in glob.players: if p is not t: p.enqueue(packets.userPresence(t))
async def ping(p: Player, pr: BanchoPacketReader) -> None: # TODO: this should be last packet time, not just # ping.. this handler shouldn't even exist lol p.ping_time = int(time.time()) # osu! seems to error when i send nothing back, # so perhaps the official bancho implementation # expects something like a stats update.. i'm # just gonna ping it back, as i don't really # want to something more expensive so often lol p.enqueue(b'\x04\x00\x00\x00\x00\x00\x00')
async def channelJoin(p: Player, pr: PacketReader) -> None: chan_name, = await pr.read(osuTypes.string) c = glob.channels.get(chan_name) if not c or not await p.join_channel(c): await plog(f'{p} failed to join {chan_name}.', Ansi.YELLOW) return # Enqueue new channelinfo (playercount) to a ll players. #glob.players.enqueue(await packets.channelInfo(*c.basic_info)) # Enqueue channelJoin to our player. p.enqueue(await packets.channelJoin(c.name))
async def handle(self, p: Player) -> None: if not 0 <= self.match_id < 64: if self.match_id >= 64: # NOTE: this function is unrelated to mp. await check_menu_option(p, self.match_id) p.enqueue(packets.matchJoinFail()) return if not (m := glob.matches[self.match_id]): log(f'{p} tried to join a non-existant mp lobby?') p.enqueue(packets.matchJoinFail()) return
async def join_match(user: Player, p: bytes) -> None: d = reader.handle_packet(p, (('id', osuTypes.i32), ('pw', osuTypes.string),)) _id = d['id'] pw = d['pw'] if _id >= 1000: if not (menu := glob.menus.get(_id)): # TODO: use pw instead of id return user.enqueue(writer.matchJoinFail()) ret = await menu.handle(user) # if we don't return a join failure also, its gonna think we are still in lobby if isinstance(ret, str): # return string message? user.enqueue( writer.sendMessage( fromname = glob.bot.name, msg = ret, tarname = user.name, fromid = glob.bot.id ) ) return user.enqueue(writer.matchJoinFail()) user.enqueue(writer.matchJoinFail()) return ret
async def handle(self, p: Player) -> None: if not glob.matches.append(self.match): # failed to create match (match slots full). await p.send(glob.bot, 'Failed to create match (no slots available).') 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() await p.join_match(self.match, self.match.passwd) log(f'{p} created a new multiplayer match.')
async def create_match(user: Player, p: bytes) -> None: match = (reader.handle_packet(p, (('match', osuTypes.match),)))['match'] glob.matches[match.id] = match if not glob.matches.get(match.id): return user.enqueue(writer.matchJoinFail()) mp_chan = Channel(name='#multiplayer', desc=f'Multiplayer channel for match ID {match.id}', auto=False, perm=False) glob.channels[f'#multi_{match.id}'] = mp_chan match.chat = mp_chan user.join_match(match, match.pw) log(f'{user.name} created a new multiplayer lobby.', Ansi.LBLUE)
async def lastFM(p: Player, conn: AsyncConnection) -> Optional[bytes]: if conn.args['b'][0] != 'a': # not anticheat related, tell the # client not to send any more for now. return b'-3' flags = ClientFlags(int(conn.args['b'][1:])) if flags & (ClientFlags.HQAssembly | ClientFlags.HQFile): # Player is currently running hq!osu; could possibly # be a separate client, buuuut prooobably not lol. await p.ban(glob.bot, f'hq!osu running ({flags})') return b'-3' if flags & ClientFlags.RegistryEdits: # Player has registry edits left from # hq!osu's multiaccounting tool. This # does not necessarily mean they are # using it now, but they have in the past. if random.randrange(32) == 0: # Random chance (1/32) for a ban. await p.ban(glob.bot, f'hq!osu relife 1/32') return b'-3' p.enqueue(packets.notification('\n'.join([ "Hey!", "It appears you have hq!osu's multiaccounting tool (relife) enabled.", "This tool leaves a change in your registry that the osu! client can detect.", "Please re-install relife and disable the program to avoid possible ban." ]))) await p.logout() return b'-3' """ These checks only worked for ~5 hours from release. rumoi's quick!
async def create_match(user: Player, p: bytes) -> None: match = (reader.handle_packet(p, (("match", osuTypes.match),)))["match"] glob.matches[match.id] = match if not glob.matches.get(match.id): return user.enqueue(writer.matchJoinFail()) mp_chan = Channel( name="#multiplayer", desc=f"Multiplayer channel for match ID {match.id}", auto=False, perm=False, ) glob.channels[f"#multi_{match.id}"] = mp_chan match.chat = mp_chan user.join_match(match, match.pw) base_info(f"{user.name} created a new multiplayer lobby.")
async def handle(self, p: Player) -> None: p.enqueue(packets.userStats(p))
async def request_stats(user: Player, p: bytes) -> None: uids = (reader.handle_packet(p, (('uids', osuTypes.i32_list),)))['uids'] for o in glob.players.unrestricted_users: if o.id != user.id and o.id in uids: user.enqueue(writer.userStats(o))
async def handle(self, p: Player) -> None: for pid in self.user_ids: if t := await glob.players.get(id=pid): p.enqueue(packets.userPresence(t))
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))
async def lobbyJoin(p: Player, pr: BanchoPacketReader) -> None: p.in_lobby = True for m in filter(lambda m: m is not None, glob.matches): p.enqueue(await packets.newMatch(m))
async def presence_request_all(user: Player, _) -> None: for o in glob.players: if o.id != user.id: user.enqueue(writer.userPresence(o))
async def handle(self, p: Player) -> None: if p.silenced: log(f'{p} sent a message while silenced.', Ansi.LYELLOW) return # remove leading/trailing whitespace msg = self.msg.msg.strip() target = self.msg.target if target == '#spectator': if p.spectating: # we are spectating someone spec_id = p.spectating.id elif p.spectators: # we are being spectated spec_id = p.id else: return t_chan = glob.channels[f'#spec_{spec_id}'] elif target == '#multiplayer': if not p.match: # they're not in a match? return t_chan = p.match.chat else: t_chan = glob.channels[target] if not t_chan: log(f'{p} wrote to non-existent {target}.', Ansi.LYELLOW) return if p.priv & t_chan.write_priv != t_chan.write_priv: log(f'{p} wrote to {target} with insufficient privileges.') return # limit message length to 2k chars # perhaps this could be dangerous with !py..? if len(msg) > 2000: msg = f'{msg[:2000]}... (truncated)' p.enqueue( packets.notification('Your message was truncated\n' '(exceeded 2000 characters).')) cmd = (msg.startswith(glob.config.command_prefix) and await commands.process_commands(p, t_chan, msg)) if cmd: # a command was triggered. if not cmd['hidden']: t_chan.send(msg, sender=p) if 'resp' in cmd: t_chan.send_bot(cmd['resp']) else: staff = glob.players.staff t_chan.send_selective(msg=msg, sender=p, targets=staff - {p}) if 'resp' in cmd: t_chan.send_selective(msg=cmd['resp'], sender=glob.bot, targets=staff | {p}) else: # no commands were triggered # check if the user is /np'ing a map. # even though this is a public channel, # we'll update the player's last np stored. if match := regexes.now_playing.match(msg): # the player is /np'ing a map. # save it to their player instance # so we can use this elsewhere owo.. bmap = await Beatmap.from_bid(int(match['bid'])) if bmap: # parse mode_vn int from regex if match['mode_vn'] is not None: mode_vn = { 'Taiko': 1, 'CatchTheBeat': 2, 'osu!mania': 3 }[match['mode_vn']] else: # use beatmap mode if not specified mode_vn = bmap.mode.as_vanilla p.last_np = { 'bmap': bmap, 'mode_vn': mode_vn, 'timeout': time.time() + 300 # 5mins } else: # time out their previous /np p.last_np['timeout'] = 0 t_chan.send(msg, sender=p)
msg = 'Could not find map.' # time out their previous /np p.last_np['timeout'] = 0 p.send(msg, sender=t) else: # target is not aika, send the message normally if online if t.online: t.send(msg, sender=p) else: # inform user they're offline, but # will receive the mail @ next login. p.enqueue( packets.notification( f'{t.name} is currently offline, but will ' 'receive your messsage on their next login.')) # insert mail into db, # marked as unread. await glob.db.execute( 'INSERT INTO `mail` ' '(`from_id`, `to_id`, `msg`, `time`) ' 'VALUES (%s, %s, %s, UNIX_TIMESTAMP())', [p.id, t.id, msg]) await p.update_latest_activity() log(f'{p} @ {t}: {msg}', Ansi.LCYAN, fd='.data/logs/chat.log') @register class LobbyPart(BanchoPacket, type=Packets.OSU_PART_LOBBY):
async def switch(p: Player, c: Messageable, msg: Sequence[str]) -> str: if len(msg) != 1: return 'Invalid syntax: !switch <ip>' p.enqueue(await packets.switchTournamentServer(msg[0])) return 'Have a nice journey..'
async def userPresenceRequest(p: Player, pr: BanchoPacketReader) -> None: for pid in await pr.read(osuTypes.i32_list): if t := await glob.players.get_by_id(pid): p.enqueue(await packets.userPresence(t))
async def handle(self, p: Player) -> None: p.in_lobby = True for m in [_m for _m in glob.matches if _m]: p.enqueue(packets.newMatch(m))
# add to `stats` table. await glob.db.execute('INSERT INTO stats ' '(id) VALUES (%s)', [user_id]) p = Player(id=user_id, name=username, silence_end=0, priv=Privileges.Normal, osu_version=osu_ver) plog(f'{p} has registered!', Ansi.LGREEN) # enqueue registration message to the user. _msg = registration_msg.format(glob.players.staff) p.enqueue(await packets.sendMessage(glob.bot.name, _msg, p.name, p.id)) data = bytearray( await packets.userID(p.id) + await packets.protocolVersion(19) + await packets.banchoPrivileges(p.bancho_priv) + await packets.notification( f'Welcome back to the gulag!\nCurrent build: {glob.version}') + # tells osu! to load channels from config, I believe? await packets.channelInfoEnd()) # channels for c in glob.channels: if not p.priv & c.read: continue # no priv to read
async def presence_request(user: Player, p: bytes) -> None: uids = (reader.handle_packet(p, (('uids', osuTypes.i32_list),)))['uids'] for u in uids: if o := await glob.players.get(id=u): user.enqueue(writer.userPresence(o))
async def statsUpdateRequest(p: Player, pr: BanchoPacketReader) -> None: p.enqueue(await packets.userStats(p))