async def on_start() -> None: glob.http = aiohttp.ClientSession(json_serialize=orjson.dumps) # connect to mysql glob.db = cmyui.AsyncSQLPool() await glob.db.connect(glob.config.mysql) # run the sql updater updater = Updater(glob.version) await updater.run() await updater.log_startup() # 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 = float(0x7fffffff) glob.players.append(glob.bot) # add all channels from db. async for chan in glob.db.iterall('SELECT * FROM channels'): chan['read_priv'] = Privileges(chan.pop('read_priv', 1)) chan['write_priv'] = Privileges(chan.pop('write_priv', 2)) glob.channels.append(Channel(**chan)) # add all mappools from db. async for pool in glob.db.iterall('SELECT * FROM tourney_pools'): # overwrite basic types with some class types creator = await glob.players.get(id=pool['created_by'], sql=True) pool['created_by'] = creator # replace id with player object pool = MapPool(**pool) await pool.maps_from_sql() glob.pools.append(pool) # 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, 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) query = ('SELECT id, donor_end FROM users ' 'WHERE donor_end > UNIX_TIMESTAMP()') async for donation in glob.db.iterall(query): # calculate the delta between now & the exp date. delta = donation['donor_end'] - time.time() if delta > (60 * 60 * 24 * 30): # ignore donations expiring in over a months time; # the server should restart relatively often anyways. continue asyncio.create_task(rm_donor(donation['id'], delta))
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 prepare(cls, db_cursor: aiomysql.DictCursor) -> 'Channels': """Fetch data from sql & return; preparing to run the server.""" log('Fetching channels from sql.', Ansi.LCYAN) await db_cursor.execute('SELECT * FROM channels') 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) async for row in db_cursor ])
async def prepare(cls, db_cursor: aiomysql.DictCursor) -> "Channels": """Fetch data from sql & return; preparing to run the server.""" log("Fetching channels from sql.", Ansi.LCYAN) await db_cursor.execute("SELECT * FROM channels") 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, ) async for row in db_cursor ], )
async def setpriv(p: 'Player', c: Messageable, msg: Sequence[str]) -> str: """Set privileges for a specified player (by name).""" if len(msg) < 2: return 'Invalid syntax: !setpriv <name> <role1 role2 role3 ...>' priv = Privileges(0) for m in msg[1:]: if not (_priv := str_priv_dict[m]): return f'Not found: {m}.' priv |= _priv
async def wrapper(*, sender: Dict[str, Any], match_id: int, **kwargs) -> Any: # TODO: Walrus can = Privileges(sender["privileges"]).has( Privileges.USER_TOURNAMENT_STAFF) if not can: match_info = await singletons.bot.Bot( ).bancho_api_client.get_match_info(match_id) can = match_info["host_api_identifier"] == sender["api_identifier"] \ or match_info["api_owner_user_id"] == sender["user_id"] if not can: return "You must be the host of the match to trigger this command." return await f(sender=sender, match_id=match_id, **kwargs)
def __init__(self, **kwargs) -> None: self.token: str = kwargs.get('token', str(uuid.uuid4())) self.id: Optional[int] = kwargs.get('id', None) self.name: Optional[str] = kwargs.get('name', None) self.safe_name = self.make_safe(self.name) if self.name else None self.priv = Privileges(kwargs.get('priv', Privileges.Normal)) self.stats = {mode: ModeData() for mode in GameMode} self.status = Status() self.friends = set() # userids, not player objects self.channels = [] self.spectators = [] self.spectating: Optional[Player] = None self.match: Optional[Match] = None # Store most recent score for each gamemode. self.recent_scores = {mode: None for mode in GameMode} # Store the last beatmap /np'ed by the user. self.last_np: Optional[Beatmap] = None self.country = (0, 'XX') # (code , letters) self.location = (0.0, 0.0) # (lat, long) self.utc_offset: int = kwargs.get('utc_offset', 0) self.pm_private: bool = kwargs.get('pm_private', False) self.away_msg: Optional[str] = None self.silence_end: int = kwargs.get('silence_end', 0) self.in_lobby = False _ctime = int(time.time()) self.login_time = _ctime self.ping_time = _ctime self.osu_version = kwargs.get('osu_version', None) self.pres_filter = PresenceFilter.Nil # XXX: below is mostly gulag-specific & internal stuff # {id: {'callback', func, 'timeout': unixt, 'reusable': False}, ...} self.menu_options: dict[int, dict[str, Any]] = {} # Packet queue self._queue = asyncio.Queue()
def __init__(self, **kwargs) -> None: self.token: str = kwargs.get('token', ''.join(choices(ascii_lowercase, k=32))) self.id: Optional[int] = kwargs.get('id', None) self.name: Optional[str] = kwargs.get('name', None) self.safe_name: Optional[str] = self.ensure_safe( self.name) if self.name else None self.priv = Privileges(kwargs.get('priv', Privileges.Normal)) self.rx = False # stored for ez use self.stats = [ModeData() for _ in range(7)] self.status = Status() self.friends = set() # userids, not player objects self.channels = [] self.spectators = [] self.spectating: Optional[Player] = None self.match: Optional[Match] = None # Store most recent score for each gamemode. self.recent_scores = [None for _ in range(7)] # Store the last beatmap /np'ed by the user. self.last_np: Optional[Beatmap] = None self.country = (0, 'XX') # (code , letters) self.location = (0.0, 0.0) # (lat, long) self.utc_offset: int = kwargs.get('utc_offset', 0) self.pm_private: bool = kwargs.get('pm_private', False) self.away_msg: Optional[str] = None self.silence_end: int = kwargs.get('silence_end', 0) self.in_lobby = False c_time = int(time()) self.login_time = c_time self.ping_time = c_time del c_time self.pres_filter = PresenceFilter.Nil # Packet queue self._queue = SimpleQueue()
async def wrapper(*, sender: Dict[str, Any], **kwargs) -> Any: if not Privileges(sender["privileges"]).has(required_privileges): return "You don't have the required privileges to trigger this command." return await f(sender=sender, **kwargs)
return p if not sql: # don't fetch from sql # if not specified return # try to get from sql. res = await glob.db.fetch( 'SELECT id, name, priv, pw_bcrypt, silence_end ' f'FROM users WHERE {attr} = %s', [val]) if not res: return priv = Privileges(res.pop('priv')) return Player(**res, priv=priv) async def get_login(self, name: str, pw_md5: str, sql: bool = False) -> Optional[Player]: # only used cached results - the user should have # logged into bancho at least once. (This does not # mean they're logged in now). if not (p := await self.get(name=name, sql=sql)): return # no such player online if glob.cache['bcrypt'][p.pw_bcrypt] == pw_md5.encode(): return p
if len(msg) < 2: return 'Invalid syntax: !setpriv <name> <role1 | role2 | ...>' # A mess that gets each unique privilege out of msg. # TODO: rewrite to be at least a bit more readable.. priv = [str_to_priv(i) for i in set(''.join(msg[1:]).replace(' ', '').lower().split('|'))] if any(x is None for x in priv): return 'Invalid privileges.' if not (t := await glob.players.get_by_name(msg[0], sql=True)): return 'Could not find user.' await glob.db.execute('UPDATE users SET priv = %s WHERE id = %s', [newpriv := sum(priv), t.id]) t.priv = Privileges(newpriv) return 'Success.' # temp command @command(priv=Privileges.Dangerous, public=False) async def menu(p: Player, c: Messageable, msg: Sequence[str]) -> str: async def callback(): p.enqueue(await packets.notification('yay!')) opt_id = await p.add_to_menu(callback) return f'[osu://dl/{opt_id} option]' # XXX: This actually comes in handy sometimes, I initially # wrote it completely as a joke, but I might keep it in for # devs.. Comes in handy when debugging to be able to run something # like `!ev print(await glob.players.get_by_name('cmyui').status.action)`
if not sql: # don't fetch from sql # if not specified return # try to get from sql. res = await glob.db.fetch( 'SELECT id, name, priv, pw_bcrypt, ' 'silence_end, clan_id, clan_rank ' f'FROM users WHERE {attr} = %s', [val]) if not res: return # overwrite some things with classes res['priv'] = Privileges(res.pop('priv')) if res['clan_id'] != 0: res['clan'] = glob.clans.get(id=res['clan_id']) res['clan_rank'] = ClanRank(res['clan_rank']) else: res['clan'] = res['clan_rank'] = None return Player(**res) async def get_ensure(self, **kwargs) -> Optional[Player]: """Try to get player from cache, or sql as fallback.""" if p := self.get(**kwargs): return p elif p := await self.get_sql(**kwargs): return p
async def setup_collections() -> None: """Setup & cache many global collections (mostly from sql).""" # create our bot & append it to the global player list. res = await glob.db.fetch('SELECT name FROM users WHERE id = 1') # global players list glob.players = PlayerList() glob.bot = Player( id=1, name=res['name'], priv=Privileges.Normal, last_recv_time=float(0x7fffffff) # never auto-dc ) glob.players.append(glob.bot) # global channels list glob.channels = ChannelList() async for res in glob.db.iterall('SELECT * FROM channels'): chan = Channel(name=res['name'], topic=res['topic'], read_priv=Privileges(res['read_priv']), write_priv=Privileges(res['write_priv']), auto_join=res['auto_join'] == 1) glob.channels.append(chan) # global matches list glob.matches = MatchList() # global clans list glob.clans = ClanList() async for res in glob.db.iterall('SELECT * FROM clans'): clan = Clan(**res) await clan.members_from_sql() glob.clans.append(clan) # global mappools list glob.pools = MapPoolList() async for res in glob.db.iterall('SELECT * FROM tourney_pools'): pool = MapPool(id=res['id'], name=res['name'], created_at=res['created_at'], created_by=await glob.players.get_ensure(id=res['created_by'])) await pool.maps_from_sql() glob.pools.append(pool) # global achievements (sorted by vn gamemodes) glob.achievements = {0: [], 1: [], 2: [], 3: []} async for res in glob.db.iterall( 'SELECT `id`, `file`, `name`, `desc`, `cond`, `mode` FROM achievements' ): # NOTE: achievement conditions are stored as # stringified python expressions in the database # to allow for easy custom achievements. condition = eval(f'lambda score: {res.pop("cond")}') achievement = Achievement(**res, cond=condition) # NOTE: achievements are grouped by modes internally. glob.achievements[res['mode']].append(achievement) """ XXX: Unfinished code for beatmap submission.
async def on_start() -> None: glob.http = aiohttp.ClientSession(json_serialize=orjson.dumps) # connect to mysql glob.db = cmyui.AsyncSQLPool() await glob.db.connect(glob.config.mysql) # run the sql updater updater = Updater(glob.version) await updater.run() await updater.log_startup() # 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 = float(0x7fffffff) glob.players.append(glob.bot) # TODO: this section is getting a bit gross.. :P # should be moved and probably refactored pretty hard. # add all channels from db. async for c_res in glob.db.iterall('SELECT * FROM channels'): c_res['read_priv'] = Privileges(c_res.get('read_priv', 1)) c_res['write_priv'] = Privileges(c_res.get('write_priv', 2)) glob.channels.append(Channel(**c_res)) # add all mappools from db. async for p_res in glob.db.iterall('SELECT * FROM tourney_pools'): # overwrite basic types with some class types creator = await glob.players.get(id=p_res['created_by'], sql=True) p_res['created_by'] = creator # replace id with player object pool = MapPool(**p_res) await pool.maps_from_sql() glob.pools.append(pool) # add all clans from db. async for c_res in glob.db.iterall('SELECT * FROM clans'): # fetch all members from sql m_res = await glob.db.fetchall( 'SELECT id, clan_rank ' 'FROM users ' 'WHERE clan_id = %s', c_res['id'], _dict=False) members = set() for p_id, clan_rank in m_res: if clan_rank == 3: c_res['owner'] = p_id members.add(p_id) glob.clans.append(Clan(**c_res, members=members)) # add all achievements from db. async for a_res in glob.db.iterall('SELECT * FROM achievements'): condition = eval(f'lambda score: {a_res.pop("cond")}') achievement = Achievement(**a_res, cond=condition) glob.achievements[a_res['mode']].append(achievement) """ bmsubmit stuff # get the latest set & map id offsets for custom maps. maps_res = await glob.db.fetch( 'SELECT id, set_id FROM maps ' 'WHERE server = "gulag" ' 'ORDER BY id ASC LIMIT 1' ) if maps_res: glob.gulag_maps = maps_res """ # 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, 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) query = ('SELECT id, donor_end FROM users ' 'WHERE donor_end > UNIX_TIMESTAMP()') async for donation in glob.db.iterall(query): # calculate the delta between now & the exp date. delta = donation['donor_end'] - time.time() if delta > (60 * 60 * 24 * 30): # ignore donations expiring in over a months time; # the server should restart relatively often anyways. continue asyncio.create_task(rm_donor(donation['id'], delta))
async def match_user_joined(match: Dict[str, Any], tournament_match: misirlou.Match, user: Dict[str, Any], **_) -> None: # Determine the slot of whoever joined the match new_user = user new_slot = None new_slot_idx = None for i, slot in enumerate(match["slots"]): if slot["user"]["api_identifier"] == new_user["api_identifier"]: new_slot = slot new_slot_idx = i break assert new_slot is not None and new_slot_idx is not None # Check if they are a player the_team = tournament_match.get_user_team(new_user["user_id"]) if the_team is not None: # Team player, move them in the right spot # TODO: If there's another client from the same user, kick the old one if len(the_team.members_in_match ) >= tournament_match.tournament.team_size: # Too many players in this team, this player is not needed. Kick them. await bot.bancho_api_client.match_kick( tournament_match.bancho_match_id, new_user["api_identifier"]) bot.send_message( "Your team is full, please ask one of your teammates " "to leave the match if you want to play instead.", new_user["api_identifier"]) return # Ok, move them to the right free slot for this team # Team A gets the first half of the slots, Team B gets the second half first_slot = tournament_match.tournament.team_size * ( 0 if the_team.enum == MisirlouTeam.A else 1) new_new_slot_idx = None for s_i, the_slot in enumerate( match["slots"][first_slot:first_slot + tournament_match.tournament.team_size]): if SlotStatus(the_slot["status"]) == SlotStatus.OPEN or ( the_slot["user"] is not None and the_slot["user"]["api_identifier"] == new_user["api_identifier"]): new_new_slot_idx = first_slot + s_i break # A slot must exist! assert new_new_slot_idx is not None # If different slot, move if new_slot_idx != new_new_slot_idx: await bot.bancho_api_client.match_move_user( tournament_match.bancho_match_id, new_user["api_identifier"], new_new_slot_idx) # Also set the team (always) await bot.bancho_api_client.set_team(tournament_match.bancho_match_id, new_user["api_identifier"], the_team.enum.bancho_team) # Update state the_team.members_in_match.add(new_user["user_id"]) tournament_match.usernames[new_user["user_id"]] = new_user["username"] # Greet the user with a notification if tournament_match.state == TournamentState.PRE_MATCH: await bot.bancho_api_client.alert( new_user["api_identifier"], f"@@@ {tournament_match.tournament.name} @@@\n\n" "Welcome to your tournament match!\nThe match will begin as soon as all the players show up. " "Please be ready to start playing and don't go afk. The match is managed by an automated bot. " "If you need any kind of assistance you can call a human referee with the command '!t humanref'.\n\n" "Have fun and good luck!") # Notify the bot if the teams are full if len(the_team.members_in_match) == \ len(tournament_match.team_enum_to_team(the_team.enum.other).members_in_match) == \ tournament_match.tournament.team_size: tournament_match.state = TournamentState.ROLLING bot.client.trigger("tournament_match_full", match_id=tournament_match.bancho_match_id) return # Not a tournament player, allow only refs if not Privileges(new_user["privileges"]).has( Privileges.USER_TOURNAMENT_STAFF): # Who is this person? Who called them? await bot.bancho_api_client.match_kick( tournament_match.bancho_match_id, new_user["api_identifier"]) await bot.bancho_api_client.alert( new_user["api_identifier"], "This is a tournament match and you are not allowed to be in there." ) return # Tournament staff, move them in the last free slot so they don't mess up # with the tournament client view last_free_slot_idx = None for slot_idx, slot in reversed(list(enumerate(match["slots"]))): if SlotStatus(slot["status"]).has(SlotStatus.OPEN): last_free_slot_idx = slot_idx break if last_free_slot_idx is None: # No slot? don't move them, they won't hurt anyone plugins.tournament.logger.warning( "No more free slots available in the tournament match?") else: await bot.bancho_api_client.match_move_user( tournament_match.bancho_match_id, new_user["api_identifier"], last_free_slot_idx, )