def test_reset_tournament_team(self): tournament = self._create_default_tournament() team = tournament.teams[0] team.captain = "Captain" team.lineup = [Player(name="P1")] toornament_c_mock = Mock() tournament_service = TournamentService(toornament_c_mock) self.assertRaises( TournamentTeamIDNotFound, tournament_service.reset_tournament_team, tournament, 99, ) toornament_c_mock.get_participant.return_value = None self.assertRaises( ErrorFetchingParticipantData, tournament_service.reset_tournament_team, tournament, int(team.id), ) get_participant = load_resource("get_participant.json") toornament_c_mock.get_participant.return_value = get_participant self.assertIsNotNone(team.captain) self.assertEqual(1, len(team.lineup)) tournament_service.reset_tournament_team(tournament, int(team.id)) self.assertIsNone(team.captain) self.assertEqual(2, len(team.lineup))
def test_remove_channel(self): tournament = Tournament(alias="Test", id=123, channels=["a", "b"]) tournament_service = TournamentService(Mock()) self.assertEqual(2, len(tournament.channels)) self.assertRaises(TournamentRoleNotFound, tournament_service.remove_admin_role, tournament, "c") tournament_service.remove_channel(tournament, "b") self.assertEqual(1, len(tournament.channels))
def _create_default_tournament( alias: str = "Test Tournament") -> Tournament: get_tournament = load_resource("get_tournament.json") get_participants = load_resource("get_participants.json") toornament_c_mock = Mock() toornament_c_mock.get_tournament.return_value = get_tournament toornament_c_mock.get_participants.return_value = get_participants tournament_service = TournamentService(toornament_c_mock) return tournament_service.create_tournament(get_tournament.get("id"), alias)
def test_remove_admin_role(self): tournament_service = TournamentService(Mock()) tournament = Tournament(alias="Test", id=123, administrator_roles=["a", "b"]) self.assertEqual(2, len(tournament.administrator_roles)) self.assertRaises(TournamentRoleNotFound, tournament_service.remove_admin_role, tournament, "c") tournament_service.remove_admin_role(tournament, "b") self.assertEqual(1, len(tournament.administrator_roles))
def test_remove_match(self): match_name = "Match" tournament = Tournament(alias="Test", id=123) tournament.matches.append(Match(name=match_name, created_by="Test")) self.assertEqual(1, len(tournament.matches)) tournament_service = TournamentService(Mock()) self.assertRaises( TournamentMatchNameNotFound, tournament_service.remove_match, tournament, "Unknown", ) tournament_service.remove_match(tournament, match_name) self.assertEqual(0, len(tournament.matches))
def test_create_tournament(self): # prepare get_tournament = load_resource("get_tournament.json") get_participants = load_resource("get_participants.json") self.assertEqual(4, len(get_participants)) toornament_c_mock = Mock() toornament_c_mock.get_tournament.return_value = get_tournament toornament_c_mock.get_participants.return_value = get_participants tournament_service = TournamentService(toornament_c_mock) alias = "Test Tournament" # execute tournament = tournament_service.create_tournament( get_tournament.get("id"), alias) # assert self.assertEqual(alias, tournament.alias) self.assertEqual(get_tournament.get("id"), tournament.id) self.assertEqual(4, len(tournament.teams))
def test_remove_tournament_team(self): # prepare tournament = self._create_default_tournament() self.assertEqual(4, len(tournament.teams), "Tournament should have 4 teams") tournament_service = TournamentService(Mock()) # execute team = tournament_service.remove_tournament_team(tournament, 3) # assert self.assertEqual(3, int(team.id), "Should find the right ID") self.assertEqual(3, len(tournament.teams), "Tournament should now have 3 teams") self.assertRaises( TournamentTeamIDNotFound, tournament_service.remove_tournament_team, tournament, 99, )
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.toornament_api_client = ToornamentAPIClient() self.tournament_service = TournamentService(self.toornament_api_client) self.match_service = MatchService(self.toornament_api_client)
class TournamentManagerPlugin(BotPlugin): toornament_api_client = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.toornament_api_client = ToornamentAPIClient() self.tournament_service = TournamentService(self.toornament_api_client) self.match_service = MatchService(self.toornament_api_client) def activate(self): """ Triggers on plugin activation """ super(TournamentManagerPlugin, self).activate() if "tournaments" not in self: self["tournaments"] = {} @arg_botcmd("role", type=str, nargs="+") @arg_botcmd("alias", type=str, admin_only=True) @tournament_admin_only def add_admin_role(self, msg, alias: str, role: List[str]): """ [Admin] Link a Discord admin role to a tournament. E.g. `!add admin role fortnite Fortnite Admin` """ role = " ".join(role) if not self._bot.find_role(role): return f"Role `{role}` not found" try: with update_tournament(self, alias) as tournament: if role in tournament.administrator_roles: return ( f"Role `{role}` is already a tournament administrator role " f"of `{tournament.alias}`") tournament.administrator_roles.append(role) except AppError as err: return err return (f"Role `{role}` successfully added " f"to the tournament `{tournament.alias}`") @arg_botcmd("role", type=str, nargs="+") @arg_botcmd("alias", type=str) @tournament_admin_only def add_captain_role(self, msg, alias: str, role: List[str]): """ [Admin] Link a Discord role as a tournament Captain role. Players that will link their Discord account with their team for this tournament will also be assigned the Captain role. E.g. `!add captain fortnite Fortnite Captain` """ role = " ".join(role) if not self._bot.find_role(role): return f"Role `{role}` not found" try: with update_tournament(self, alias) as tournament: self.tournament_service.set_captain_role(tournament, role) except AppError as err: return err return (f"Captain role `{role}` successfully added " f"to the tournament `{tournament.alias}`") @arg_botcmd("channel", type=str) @arg_botcmd("alias", type=str) @tournament_admin_only def add_channel(self, msg, alias, channel): """ [Admin] Link a Discord channel to a tournament. E.g. `!add channel fortnite #fortnite-tournament` """ if not self.query_room(channel): return "Invalid channel name" try: with update_tournament(self, alias) as tournament: self.tournament_service.add_channel(tournament, channel) except AppError as err: return err return f"Channels successfully added to the tournament" @arg_botcmd("tournament_id", type=int) @arg_botcmd("alias", type=str, admin_only=True) @tournament_admin_only def add_tournament(self, msg: Message, alias, tournament_id: int): """ [Admin] `!add tournament fortnite 123456789` """ if alias in self["tournaments"]: return "Tournament with this alias already exists." try: tournament = self.tournament_service.create_tournament( tournament_id, alias) except AppError as err: return err self._save_tournament(alias, tournament) self.send(msg.frm, f"Tournament `{tournament.info.name}` successfully added") @arg_botcmd("match_id", type=int, nargs="?") @arg_botcmd("password", type=str) @arg_botcmd("match_name", type=str) @arg_botcmd("alias", type=str) @tournament_admin_only def create_match(self, msg, alias, match_name, password, match_id): """ [Admin] Create a tournament match. E.g. `!create match fortnite match_1 secretPassword 123456789` """ try: with update_tournament(self, alias) as tournament: match = self.tournament_service.find_match_by_name( tournament, match_name) if match: return "Match name already exists" match = self.match_service.create_match( tournament_id=tournament.id, match_id=match_id, match_name=match_name, created_by=msg.frm.fullname, password=password, ) tournament.matches.append(match) except AppError as err: return err self.send(msg.frm, f"Match `{match_name}` successfully created.") self._show_match(msg, tournament, match) @arg_botcmd("match_name", type=str) @arg_botcmd("alias", type=str) @tournament_admin_only def download_match_scores(self, msg, alias: str, match_name: str): """ [Admin] Download a match score submissions. E.g. `!download match scores fortnite match_1` """ if alias not in self["tournaments"]: return "Tournament not found" tournament = self._get_tournament(alias) match = tournament.find_match_by_name(match_name) if not match: return f"Match `{match_name}` not found." match_scores = tournament.get_match_scores(match_name) if not match_scores: return "No score submissions found for this match." # create temporary csv to send fd, path = tempfile.mkstemp( prefix= f"{match_name}_scores_{datetime.now().strftime('%m-%d-%Y_%H-%M-%S')}_", suffix=".csv", ) try: with open(path, "w") as csvfile: fw = csv.writer(csvfile, delimiter=",") fw.writerow([ "Team Name", "Updated at", "Position", "Eliminations", "Points", "Screenshots", ]) for ms in match_scores: fw.writerow([ ms.team_name, ms.updated_at, ms.position, ms.eliminations, ms.count_points(), " ".join(ms.screenshot_links), ]) self._bot.send_file(self.build_identifier(msg.frm.fullname), filepath=path) except Exception as e: logger.error(e) return f"Error: {e}" finally: os.remove(path) @arg_botcmd("match_name", type=str) @tournament_channel_only def join(self, msg: Message, match_name: str): """ [Linked] Join a match. E.g. `!join match_1` """ try: captain_name = msg.frm.fullname alias = self.tournament_service.get_captain_tournament_alias( self["tournaments"], captain_name) with update_tournament(self, alias) as tournament: match = self.tournament_service.get_match_by_name( tournament, match_name) team = self.tournament_service.get_captain_team( tournament, captain_name) match = self.match_service.join_match(match, int(team.id), team.name) except AppError as err: return err self.send( msg.frm, f"Team `{team.name}` is now ready for the match {match.name}!") self._show_match(msg, tournament, match) @arg_botcmd("match_name", type=str) @tournament_channel_only def leave(self, msg: Message, match_name: str): """ [Linked] Leave a joined match (if joined by mistake). Available when the match status is set to PENDING. E.g. `!leave match_1` """ with self.mutable("tournaments") as tournaments: team, tournament = self._find_captain_team(msg.frm.fullname, tournaments) if not team: return "You are not a team captain." match = tournament.find_match_by_name(match_name) if not match: return "Match not found" if team.id not in match.teams_joined: return f"Team `{team.name}` is not in this match" if match.status != MatchStatus.PENDING: return f"Can't leave match with status `{match.status.name}`" match.teams_joined.remove(team.id) # Save tournament changes to db tournaments.update({tournament.alias: tournament.to_dict()}) self.send( msg.frm, f"Team `{team.name}` has left the match `{match.name}`!") @arg_botcmd("team_name", type=str, nargs="+") @arg_botcmd("alias", type=str) @tournament_channel_only def link(self, msg: Message, alias: str, team_name: List[str]): """ Link your Discord account with a team to become the captain of this team. If there is a quote (`'` or `"`) in your team name, add a backslash before: `\"` E.g. `!link fortnite Team Liquid` """ team_name = " ".join(team_name) team, tournament = self._find_captain_team(msg.frm.fullname, self["tournaments"]) if team: return ( f"You are currently the captain of the team `{team.name}` for the " f"tournament `{alias}`") try: with update_tournament(self, alias) as tournament: team = self.tournament_service.link_team_captain( tournament, team_name, msg.frm.fullname, ) self._add_discord_team_captain(msg.frm, team_name, tournament.captain_role) except AppError as err: return err return ( f"You are now the captain of the team `{team_name}`. " f"Use `!show status` in private to display information about your team." ) @arg_botcmd("discord_user", type=str, nargs="+") @arg_botcmd("team_id", type=int) @arg_botcmd("alias", type=str) @tournament_admin_only def link_team_captain(self, msg, alias: str, team_id: int, discord_user: str): """ [Admin] Set a discord user as a team linked captain. E.g. `!link team captain fortnite 123456789 user#1234` """ discord_user = "******".join(discord_user) try: user = self.build_identifier(discord_user) except ValueError: return f"User `{discord_user}` not found." if self._find_captain_team(discord_user, self["tournaments"])[0]: return f"User `{discord_user}` is already the captain of a team." try: with update_tournament(self, alias) as tournament: team = self.tournament_service.get_team_by_id( tournament, team_id) if team.captain: self._remove_discord_team_captain( self.build_identifier(team.captain), tournament.captain_role) team.captain = discord_user self._add_discord_team_captain(user, team.name, tournament.captain_role) except AppError as err: return err return f"Team `{team.name}` captain successfully linked to `{discord_user}`." @arg_botcmd("alias", type=str) @tournament_admin_only def refresh_tournament(self, msg: Message, alias: str): """ [Admin] Refresh a tournament's information. E.g. `!refresh tournament fortnite` """ try: with update_tournament(self, alias) as tournament: tournament = self.tournament_service.refresh_tournament( tournament) except AppError as err: return err return f"Tournament {tournament.alias} successfully refreshed." @arg_botcmd("alias", type=str) @tournament_admin_only def show_tournament_refresh_status(self, msg: Message, alias: str): """ [Admin] Show difference between current Tournament and Toornament participants list """ if alias not in self["tournaments"]: return "Tournament not found" # current tournament teams ids tournament = Tournament.from_dict(self["tournaments"][alias]) tournament_team_ids = set(p.id for p in tournament.teams) # toornament participants ids participants = self.toornament_api_client.get_participants( tournament.id) toornament_participant_ids = set(p["id"] for p in participants) teams_deleted = tournament_team_ids - toornament_participant_ids teams_added = toornament_participant_ids - tournament_team_ids self.send( msg.frm, f"**Teams ID deleted:**\n" + "\n".join(i for i in teams_deleted)) self.send(msg.frm, f"**Teams ID added:**\n" + "\n".join(i for i in teams_added)) @arg_botcmd("role", type=str, nargs="+") @arg_botcmd("alias", type=str, admin_only=True) @tournament_admin_only def remove_admin_role(self, msg, alias: str, role: List[str]): """ [Admin] Remove a Discord admin role from a tournament. E.g. `!remove admin role fortnite Fortnite Admin` """ role = " ".join(role) try: with update_tournament(self, alias) as tournament: self.tournament_service.remove_admin_role(tournament, role) except AppError as err: return err return f"Roles successfully removed from the tournament `{tournament.alias}`" @arg_botcmd("alias", type=str) @tournament_admin_only def remove_captain_role(self, msg, alias): """ [Admin] Remove a tournament Discord Captain Role """ try: with update_tournament(self, alias) as tournament: tournament = self.tournament_service.remove_captain_role( tournament) except AppError as err: return err return (f"Captain Role successfully removed for " f"the tournament `{tournament.alias}`") @arg_botcmd("channel", type=str) @arg_botcmd("alias", type=str) @tournament_admin_only def remove_channel(self, msg, alias, channel): """ [Admin] Remove a linked Discord channel from a tournament. E.g. !remove channel fornite #fortnite-tournament """ try: with update_tournament(self, alias) as tournament: tournament = self.tournament_service.remove_channel( tournament, channel) except AppError as err: return err return f"Channels successfully removed from the tournament" @arg_botcmd("match_name", type=str) @arg_botcmd("alias", type=str) @tournament_admin_only def remove_match(self, msg, alias, match_name): """ [Admin] Remove a linked Discord channel from a tournament. E.g. !remove channel fornite #fortnite-tournament """ try: with update_tournament(self, alias) as tournament: tournament = self.tournament_service.remove_match( tournament, match_name) except AppError as err: return err return f"Match `{match_name}` successfully removed from the tournament" @arg_botcmd("match_name", type=str) @private_message_only def remove_score(self, msg: Message, match_name: str): """ [Linked] Remove a submitted match score. E.g. `!remove score match_1` """ with self.mutable("tournaments") as tournaments: team, tournament = self._find_captain_team(msg.frm.fullname, tournaments) if not team: return "You are not a team captain." match = tournament.find_match_by_name(match_name) if not match: return f"Match `{match_name}` not found in `{tournament.alias}`" if match.status == MatchStatus.COMPLETED: return (f"Can't delete score for match `{match_name}`. " f"Match status is set to COMPLETED.") score = team.find_submission_by_match(match_name) if not score: return f"No score found for the match `{match_name}`" team.score_submissions.remove(score) # Save tournament changes to db tournaments.update({tournament.alias: tournament.to_dict()}) return f"Score for match `{match_name}` successfully deleted." @arg_botcmd("team_id", type=int) @arg_botcmd("alias", type=str) @tournament_admin_only def remove_team(self, msg: Message, alias: str, team_id: int): """ [Admin] Remove a team from a tournament. E.g. `!remove team fortnite 123456789` """ try: with update_tournament(self, alias) as tournament: team = self.tournament_service.remove_tournament_team( tournament, team_id) if team.captain: self._remove_discord_team_captain( self.build_identifier(team.captain), tournament.captain_role) except AppError as err: return err return f"Team {team.name} successfully removed." @arg_botcmd("alias", type=str, admin_only=True) @tournament_admin_only def remove_tournament(self, msg, alias): """ [Admin] Associate a Discord role to a tournament. E.g. `!remove tournament fortnite` """ if alias not in self["tournaments"]: return "Tournament not found." with self.mutable("tournaments") as tournaments: tournaments.pop(alias) return f"Tournament successfully removed." @arg_botcmd("status", type=str) @arg_botcmd("match_name", type=str) @arg_botcmd("alias", type=str) @tournament_admin_only def set_match_status(self, msg, alias: str, match_name: str, status: str): """ [Admin] Force the match status state. E.g. !set match status fortnite match_1 completed """ try: with update_tournament(self, alias) as tournament: match = self.tournament_service.get_match_by_name( tournament, match_name) self.match_service.set_match_status(match, status) except AppError as err: return err return f"Match `{match_name}` status set to `{status.upper()}`." @arg_botcmd("team_id", type=int) @arg_botcmd("alias", type=str) @tournament_admin_only def reset_team(self, msg: Message, alias: str, team_id: int): """ [Admin] Reset a team information and linked captain. E.g. `!reset team fortnite 123456789` """ try: with update_tournament(self, alias) as tournament: team = self.tournament_service.reset_tournament_team( tournament, team_id) if team.captain: self._remove_discord_team_captain( self.build_identifier(team.captain), tournament.captain_role) except AppError as err: return err return f"Team {team.name} successfully updated." @arg_botcmd("match_name", type=str) @arg_botcmd("alias", type=str) @private_message_only def show_match(self, msg, alias: str, match_name: str): """ Show a tournament's match. E.g. `!show match fortnite match1` """ if alias not in self["tournaments"]: return "Tournament not found" tournament = Tournament.from_dict(self["tournaments"][alias]) match = tournament.find_match_by_name(match_name) if not match: return f"Match `{match_name}` not found in tournament `{tournament.alias}`" self._show_match(msg, tournament, match) @arg_botcmd("alias", type=str) @tournament_channel_only def show_matches(self, msg, alias): """ Show the matches of a tournament. E.g. `!show matches fortnite` """ if alias not in self["tournaments"]: return "Tournament not found" fields = [] tournament = Tournament.from_dict(self["tournaments"][alias]) team = tournament.find_team_by_captain(msg.frm.fullname) for match in tournament.matches: fields.append(( str(match.name), (str(match) + "*You have joined this match*" if bool( team and team.id in match.teams_joined) else str(match)), )) self.send_card(title=tournament.alias, fields=fields, in_reply_to=msg, color="grey") @arg_botcmd("match_name", type=str) @arg_botcmd("alias", type=str) @tournament_admin_only def show_match_scores(self, msg, alias: str, match_name: str): """ [Admin] Show a tournament match score submissions. E.g. `!show match scores fortnite match_1` """ if alias not in self["tournaments"]: return "Tournament not found" tournament = Tournament.from_dict(self["tournaments"][alias]) match = tournament.find_match_by_name(match_name) if not match: return f"Match `{match_name}` not found." match_scores = tournament.get_match_scores(match_name) if not match_scores: return "No score submissions found for this match." match_scores = sorted(match_scores, key=lambda k: getattr(k, "position")) match_scores_chunk = chunks(match_scores, 25) for i, chunk in enumerate(match_scores_chunk): self.send_card( title=f"{match.name} @ {tournament.alias} Score Submissions " f"({i + 1}/{len(match_scores_chunk)})", fields=((str(score.team_name), str(score)) for score in chunk), in_reply_to=msg, color="grey", ) @botcmd @private_message_only def show_scores(self, msg, *args): """ [Linked] Show your team match score submissions history. E.g. `!show scores` """ team, tournament = self._find_captain_team(msg.frm.fullname, self["tournaments"]) if not team: return "You are not linked to a team." self.send_card( title=f"{team.name} @ {tournament.alias} Score Submissions", fields=((str(score.match_name), str(score)) for score in team.score_submissions), in_reply_to=msg, color="grey", ) @botcmd @private_message_only def show_status(self, msg, args): """ Show your currently linked team and joined matched. """ team, tournament = self._find_captain_team(msg.frm.fullname, self["tournaments"]) if not team: return "You are not the captain of a team." self.send_card( title=f"{team.name} Team @ {tournament.info.name}", summary=f"To unregister, type:\n!unlink {team.name}", to=self.build_identifier(msg.frm.fullname), **team.show_card(), color="green", ) joined_matches = [] for match in tournament.matches: if team.id in match.teams_joined: joined_matches.append(match) if joined_matches: self.send_card( title=f"{team.name} Matches @ {tournament.info.name}", to=self.build_identifier(msg.frm.fullname), fields=(( match.name, (f"**Status:** {match.status.name}\n" f"**Password:** {match.password}\n" f"**Score Submission:** " f"{str(team.find_submission_by_match(match.name))}"), ) for match in joined_matches), ) @arg_botcmd("team_name", type=str, nargs="+") @arg_botcmd("alias", type=str) @tournament_channel_only def show_team(self, msg: Message, alias: str, team_name: List[str]): """ Show a team information. E.g. `!show team fortnite Team Liquid` """ if alias not in self["tournaments"]: return "Tournament not found" team_name = " ".join(team_name) with self.mutable("tournaments") as tournaments: tournament = Tournament.from_dict(tournaments[alias]) team = tournament.find_team_by_name(team_name) if not team: return ( f"Team `{team_name}` not found in the tournament `{tournament.alias}`" ) self.send_card( in_reply_to=msg, title=f"{team.name} @ {tournament.info.name}", **team.show_card(), color="grey", ) @arg_botcmd("team_id", type=int) @arg_botcmd("alias", type=str) @tournament_channel_only def show_team_by_id(self, msg: Message, alias: str, team_id: int): """ Show a team information. E.g. `!show team by id fortnite 123456789` """ if alias not in self["tournaments"]: return "Tournament not found" tournament = Tournament.from_dict(self["tournaments"][alias]) team = tournament.find_team_by_id(team_id) if not team: return f"Team `{team.name}` not found in the tournament `{tournament.alias}`" self.send_card( in_reply_to=msg, title=f"{team.name} @ {tournament.info.name}", **team.show_card(), color="grey", ) @arg_botcmd("alias", type=str) @private_message_only def show_teams(self, msg: Message, alias): """ Show teams linked on Discord. E.g. `!show teams fortnite` """ if alias not in self["tournaments"]: return "Tournament doesn't exists" tournament = Tournament.from_dict(self["tournaments"][alias]) participants = sorted(tournament.teams, key=lambda k: getattr(k, "name")) participants = [p for p in participants if p.captain is not None] if len(participants) == 0: return "No team registered for this tournament" count = 1 participants_chunks = chunks(participants, 100) for i, chunk in enumerate(participants_chunks): team_names = "" for team in chunk: team_names += f"{count}. {team.name}\n" count += 1 self.send_card( title=f"{tournament.alias} Registered Participants" f"({i + 1}/{len(participants_chunks)})", body=(f"Number of teams: " f"{len(tournament.teams)} \n" f"Number of registrations: " f"{tournament.count_linked_teams()}\n\n" f"{team_names}"), color="grey", in_reply_to=msg, ) @arg_botcmd("alias", type=str) @private_message_only def show_teams_missing(self, msg: Message, alias): """ Show teams not linked on Discord. E.g. `!show teams missing fortnite` """ if alias not in self["tournaments"]: return "Tournament doesn't exists" tournament = Tournament.from_dict(self["tournaments"][alias]) participants = sorted(tournament.teams, key=lambda k: getattr(k, "name")) participants = [p for p in participants if p.captain is None] if len(participants) == 0: return "Every team are registered for this tournament" missing_participants_count = (len(tournament.teams) - tournament.count_linked_teams()) count = 1 participants_chunks = chunks(participants, 100) for i, chunk in enumerate(participants_chunks): team_names = "" for team in chunk: team_names += f"{count}. {team.name}\n" count += 1 self.send_card( title=f"{tournament.alias} Missing Registrations " f"({i + 1}/{len(participants_chunks)})", body=(f"Number of teams: " f"{len(tournament.teams)} \n" f"Number of missing registrations: " f"{missing_participants_count}\n\n" f"{team_names}"), color="grey", in_reply_to=msg, ) @arg_botcmd("alias", type=str) def show_tournament(self, msg, alias): """ Show a tournament. E.g. `!show tournament fortnite` """ if alias not in self["tournaments"]: return "Tournament not found." tournament = self._get_tournament(alias) self._show_tournament(msg, tournament) @botcmd def show_tournaments(self, msg, args): """ Show available tournaments. E.g. `!show tournaments` """ if self["tournaments"]: for tournament in self["tournaments"].values(): tournament = Tournament.from_dict(tournament) self._show_tournament(msg, tournament) else: return "No tournaments to show." @arg_botcmd("match_name", type=str) @arg_botcmd("alias", type=str) @tournament_admin_only def start_match(self, msg, alias: str, match_name: str): """ [Admin] Start a tournament match. This will send a notification in the tournament's channels (if exists) and to the linked captains of this tournament that the match is going to start in 30 seconds. E.g. `!start match fortnite match_1` """ if alias not in self["tournaments"]: return "Tournament not found" try: with update_tournament(self, alias) as tournament: match = self.tournament_service.get_match_by_name( tournament, match_name) match = self.match_service.start_match(match) for channel in tournament.channels: room = self.query_room(channel) self.send( room, f"The match `{match_name}` will start in ~30 seconds") for team_id in match.teams_joined: team = tournament.find_team_by_id(team_id) captain_user = self.build_identifier(team.captain) self.send( captain_user, f"The match `{match_name}` for the team `{team.name}` " f"will start in ~30 seconds!", ) except AppError as err: return err self.send(msg.frm, f"Match `{match_name}` status set to ONGOING.") @arg_botcmd("team_name", type=str, nargs="+") @tournament_channel_only def unlink(self, msg: Message, team_name: List[str]): """ [Linked] Unlink your current team with your Discord account. E.g. `!unlink Team SoloMid` """ team_name = " ".join(team_name) with self.mutable("tournaments") as tournaments: team, tournament = self._find_captain_team(msg.frm.fullname, tournaments) if not team: return "You are not the captain of a team." if team.name != team_name: return ( f"Your linked team name `{team.name}` is different from " f"your entry `{team_name}`. To confirm your unregistration, please " f"type the right team name.") team.captain = None # Save tournament changes to db tournaments.update({tournament.alias: tournament.to_dict()}) self._remove_discord_team_captain(msg.frm, tournament.captain_role) return f"You are no longer the captain of the team `{team_name}`." def callback_attachment(self, msg: Message, discord_msg: discord.Message): """ Send screenshots in private message to bot """ if hasattr(msg.to, "fullname") and msg.to.fullname == str( self.bot_identifier): cmds = ["!submit", "!add", "!add_screenshot"] msg_parts = msg.body.strip().split(" ") if not msg_parts or msg_parts[0] not in cmds: self.send( msg.frm, ("Wow! Thank you so much for this beautiful attachment!\n" "Unfortunately, I'm unsure what I'm supposed to do with that." " Are you trying to submit a match score screenshot?\n" "To submit a score for a match, use: " "`submit [match_name] position [number] eliminations [number]`.\n" "To add a screenshot to your previous score submission, use: " "`!add screenshot [match_name]`.\n" "To remove a score submission for a match and add a new one, use " "`!remove score [match_name]`."), ) return # !submit if msg_parts[0] == "!submit": # validate format if len(msg_parts) != 6: self.send( msg.frm, ("Looks like you're trying to submit your score!\n\n" "Your screenshot must be followed with the following" " information: " "`!submit [match_name] position [number] " "eliminations [number]`\n"), ) return _, match_name, _, position, _, eliminations = msg_parts # invalid entries if not position.isdigit() or not eliminations.isdigit(): self.send( msg.frm, "Invalid entry for position or eliminations. " "A number was expected.", ) return try: alias = self.tournament_service.get_captain_tournament_alias( self["tournaments"], msg.frm.fullname) with update_tournament(self, alias) as tournament: team = self.tournament_service.get_captain_team( tournament, msg.frm.fullname) self.tournament_service.submit_score( tournament=tournament, match_name=match_name, team_id=int(team.id), urls=[a.url for a in discord_msg.attachments], position=int(position), eliminations=int(eliminations), ) except AppError as err: self.send(msg.frm, str(err)) return self.send( msg.frm, "Score successfully added! Type `!show scores` " "to see your score submissions history.", ) # add screenshot elif (msg_parts[0] == "!add" and msg_parts[1] == "screenshot") or (msg_parts[0] == "!add_screenshot"): if len(msg_parts) == 3: match_name = msg_parts[2] elif len(msg_parts) == 2: match_name = msg_parts[1] else: self.send( msg.frm, "Invalid command format. Use `!add screenshot [match_name]`", ) return try: alias = self.tournament_service.get_captain_tournament_alias( self["tournaments"], msg.frm.fullname) with update_tournament(self, alias) as tournament: team = self.tournament_service.get_captain_team( tournament, msg.frm.fullname) self.tournament_service.add_screenshot( tournament=tournament, match_name=match_name, team_id=int(team.id), urls=[a.url for a in discord_msg.attachments], ) except AppError as err: self.send(msg.frm, str(err)) return self.send( msg.frm, "Score successfully updated! Type `!show scores` " "to see your score submissions history.", ) @botcmd @private_message_only def help(self, msg, *args): """ Display bot commands """ # Sketchy override of the bot help command def sanitize_cmd(cmd_text: str): return " ".join(l.strip() for l in cmd_text.split("\n")) commands = [] linked_commands = [] admin_commands = [] for cmd_name, cmd in self._bot.commands.items(): # Extract command names and descriptions if not hasattr(cmd, "_err_command_parser"): # undocumented description = sanitize_cmd(self._bot.get_doc(cmd)) name = cmd_name elif cmd._err_command_parser.description: # use command docstring description = sanitize_cmd(cmd._err_command_parser.description) name = ( cmd_name + " " + " ".join(f"*[{a.dest}]*" for a in cmd._err_command_parser._actions[1:])) else: # use argparse syntax description = cmd._err_command_syntax name = ( cmd_name + " " + " ".join(f"*[{a.dest}]*" for a in cmd._err_command_parser._actions[1:])) if "[ADMIN]" in description.upper(): admin_commands.append((name, description)) elif "[LINKED]" in description.upper(): linked_commands.append((name, description)) else: commands.append((name, description)) # General Commands help_message = ( "```General Commands```" "*Bot available commands. " "\nNote: The **[alias]** parameter is the `Tournament Alias` field that can " "be found in the tournaments description. To see available tournaments, " "use `!show tournaments`*" "\n\n") help_message += "\n\n".join(f"**!{name}**\n{descr}" for name, descr in commands) self.send(msg.frm, help_message) # Linked Commands help_message = ( f"```Linked Captains Commands```" f"*Commands available to captains linked to a team*\n\n") help_message += "\n\n".join(f"**!{name}**\n{descr[10:]}" for name, descr in linked_commands) help_message += ( "\n\n**!submit *[match_name]* position *[number]* eliminations *[number]***" "\n***THIS COMMAND MUST BE SENT IN PRIVATE TO THE BOT WITH A SCREENSHOT " "ATTACHED TO IT***\n" "Submit your team score for a match. " "E.g. with an attached screenshot, add the message: `!submit game_1 position " "2 eliminations 5` to submit a score where your position is `2`nd and number " "of eliminations `5` for the match named `game_1`.\n" "- Score submission is disabled when a match status is set to COMPLETED.\n" "- If you made a mistake while submitting your score, use this command " "again to override the previous submission for this match.\n" "- The information submitted must match the one visible on your screenshot. " "Submitting a different score than what is displayed on your screenshot " "will result in a sanction.\n") self.send(msg.frm, help_message) # Admin Commands if self._is_tournament_admin(msg.frm): help_message = "```Admin Commands```\n" help_message += "\n\n".join(f"**!{name}**\n{descr[9:]}" for name, descr in admin_commands) self.send(msg.frm, help_message) self.send( msg.frm, "Commands summary are also available at this link: " "<https://docs.google.com/document/d/1eedLoQdVLVe2JkCe19g69w-UUL49iFW93mz4piypY1k/edit?usp=sharing>", # noqa ) @staticmethod def _add_discord_team_captain(user: DiscordPerson, team_name: str, captain_role: Optional[str]): """ Update Discord user nickname and add role if tournament captain_role is set """ # magic number 32, discord max username length, shhht max_length = 32 - (len(user.nick) + 4) discord_team_name = team_name[:max_length] + (team_name[max_length:] and "..") discord_captain_name = f"{user.nick}[{discord_team_name}]" # Update username nickname user.edit_nickname(discord_captain_name) if captain_role: if not user.has_guild_role(captain_role): sleep(1) user.add_role(captain_role) @staticmethod def _find_captain_team( username: str, tournaments: dict) -> Tuple[Optional[Team], Optional[Tournament]]: for t in tournaments.values(): tournament = Tournament.from_dict(t) team = tournament.find_team_by_captain(username) if team: return team, tournament return None, None @staticmethod def _remove_discord_team_captain(user: DiscordPerson, captain_role: Optional[str]): """ Reset Discord user nickname and remove role if tournament captain_role is set """ user.edit_nickname(user.username) if captain_role: if user.has_guild_role(captain_role): sleep(1) user.remove_role(captain_role) def _show_match(self, msg, tournament: Tournament, match: Match): team = tournament.find_team_by_captain(msg.frm.fullname) fields = [ ("Status", f"{match.status.name}\n"), ( "Teams Joined", f"{len(match.teams_joined)}/" f"{len(match.teams_registered)}\n", ), ("Created by", f"{match.created_by}\n"), ("Match ID", f"{match.id}\n"), ("Group", f"{match.group_name}\n"), ] has_joined_the_match = bool(team and team.id in match.teams_joined) if has_joined_the_match: fields.append(("Password", f"{match.password}\n")) self.send_card( title=f"{match.name} @ {tournament.alias}", fields=fields, to=self.build_identifier(msg.frm.fullname), color="green" if has_joined_the_match else "grey", ) def _show_tournament(self, msg, tournament: Tournament): team_status_text = "**You are not the captain of a team in this tournament*" team = tournament.find_team_by_captain(msg.frm.fullname) if team is not None: team_players = ", ".join(pl.name for pl in team.lineup) or None team_status_text = (f"**Team Name:** {team.name}\n" f"**Team Players:** {team_players}") administrators = [] for admin_role in tournament.administrator_roles: administrators.extend(self._bot.get_role_members(admin_role)) admins = ", ".join(administrators) self.send_card( body=f"{tournament.url}\n\n{team_status_text}", summary=f"Tournament Administrators:\t{admins}", color="green" if team else "grey", in_reply_to=msg, **tournament.show_card(), ) def _is_tournament_admin(self, user: DiscordPerson) -> bool: admin_roles = list( itertools.chain(*[ t["administrator_roles"] for t in self["tournaments"].values() ])) if user.fullname in self.bot_config.BOT_ADMINS or any( user.has_guild_role(r) for r in admin_roles): return True return False def _get_tournament(self, alias: str): return Tournament.from_dict(self["tournaments"][alias]) def _save_tournament(self, alias: str, tournament: Tournament): with self.mutable("tournaments") as tournaments: tournaments.update({alias: tournament.to_dict()})