public Task SetupTournamentAsync(string tournamentName) { // It's okay not to harden this too much, because they can retry the action, and their failure doesn't // make anything inconsistent. if (string.IsNullOrEmpty(tournamentName)) { this.Logger.Debug("Couldn't setup with blank name"); return(Task.CompletedTask); } TournamentsManager manager = this.GlobalManager.GetOrAdd(this.Context.Guild.Id, CreateTournamentsManager); // If the tournament didn't exist before, create it now. This must be an admin user who wanted to create // the tournament without any other directors. if (!manager.TryGetTournament(tournamentName, out ITournamentState state)) { manager.AddOrUpdateTournament(tournamentName, new TournamentState( this.Context.Guild.Id, tournamentName), (name, oldState) => oldState); } if (!manager.TrySetCurrentTournament(tournamentName, out string errorMessage)) { this.Logger.Debug("Error when setting up tournament: {errorMessage}", errorMessage); return(this.SendChannelMessage( BotStrings.ErrorSettingCurrentTournament(this.Context.Guild.Name, errorMessage))); } return(this.DoReadWriteActionOnCurrentTournamentAsync( currentTournament => this.UpdateStageAsync(currentTournament, TournamentStage.AddReaders))); }
private async Task HandleAddReadersStage(ITournamentState currentTournament, IGuild guild, SocketMessage message) { IEnumerable <Task <IGuildUser> > getReaderMembers = message.MentionedUsers .Select(user => guild.GetUserAsync(user.Id, options: RequestOptionsSettings.Default)); IGuildUser[] readerMembers = await Task.WhenAll(getReaderMembers); IEnumerable <Reader> readers = readerMembers.Select(member => new Reader() { Id = member.Id, Name = member.Nickname ?? member.Username }); currentTournament.AddReaders(readers); if (!currentTournament.Readers.Any()) { this.Logger.Debug("No readers specified, so staying in the AddReaders stage"); await message.Channel.SendMessageAsync( BotStrings.NoReadersAddedMinimumReaderCount, options : RequestOptionsSettings.Default); return; } await message.Channel.SendMessageAsync( BotStrings.ReadersTotalForTournament(currentTournament.Readers.Count())); await this.UpdateStage(currentTournament, TournamentStage.SetRoundRobins, message.Channel); }
public async Task ClearAllAsync() { await this.CleanupAllPossibleTournamentArtifactsAsync(); this.Logger.Debug("All tournament artifacts cleared"); await this.SendChannelMessage(BotStrings.AllPossibleTournamentArtifactsCleaned(this.Context.Guild.Name)); }
public async Task StartAsync() { bool startSucceeded = false; await this.DoReadWriteActionOnCurrentTournamentAsync( async currentTournament => { if (currentTournament?.Stage != TournamentStage.AddPlayers) { // !start only applies once we've started adding players this.Logger.Debug("Start failed because we were in stage {stage}", currentTournament?.Stage); await this.SendChannelMessage(BotStrings.CommandOnlyUsedTournamentReadyStart); return; } await this.UpdateStageAsync(currentTournament, TournamentStage.BotSetup); try { // TODO: Add more messaging around the current status this.Logger.Debug("Generating tournament"); IScheduleFactory scheduleFactory = new RoundRobinScheduleFactory( currentTournament.RoundRobinsCount); currentTournament.Schedule = scheduleFactory.Generate( new HashSet <Team>(currentTournament.Teams), new HashSet <Reader>(currentTournament.Readers)); this.Logger.Debug("Tournament generated. Creating channels and roles"); await this.Context.Channel.SendMessageAsync( BotStrings.CreatingChannelsAndRoles, options: RequestOptionsSettings.Default); await this.CreateArtifactsAsync(currentTournament); IUserMessage updateMessage = await this.UpdateStageAsync( currentTournament, TournamentStage.RunningTournament); await updateMessage.PinAsync(RequestOptionsSettings.Default); currentTournament.PinnedStartMessageId = updateMessage.Id; startSucceeded = true; } catch (Exception ex) { // TODO: Make the exceptions we catch more defined. // Go back to the previous stage and undo any artifacts added. this.Logger.Error(ex, "Error starting the tournament. Cleaning up the tournament artifacts"); await this.CleanupTournamentArtifactsAsync(currentTournament); await this.UpdateStageAsync(currentTournament, TournamentStage.AddPlayers); throw; } }); if (startSucceeded) { await this.Context.Channel.SendMessageAsync( BotStrings.TournamentHasStarted( MentionUtils.MentionChannel(this.Context.Channel.Id)), options : RequestOptionsSettings.Default); } }
public Task GetCurrentTournamentAsync() { // DoReadActionOnCurrentTournament will not run the action if the tournament is null. It'll send an // error message to the user instead. return(this.DoReadActionOnCurrentTournamentAsync( currentTournament => this.SendUserMessageAsync( BotStrings.CurrentTournamentInGuild(this.Context.Guild.Name, currentTournament.Name)))); }
internal async Task HandleOnReactionRemoved( Cacheable <IUserMessage, ulong> cachedMessage, ISocketMessageChannel messageChannel, SocketReaction reaction) { if (!cachedMessage.HasValue || reaction.UserId == this.Client.CurrentUser.Id) { // Ignore the bot's own additions. return; } if (!(reaction.Channel is IGuildChannel guildChannel)) { // Only listen to guild reacts. this.Logger.Verbose("Reaction removal ignored because it wasn't on a guild channel"); return; } if (!(reaction.User.IsSpecified && reaction.User.Value is IGuildUser guildUser)) { this.Logger.Verbose("Reaction removal ignored because it wasn't by a guild user"); return; } // Issue: when we remove a reaction, this will remove the team they were on. How would we know this, outside // of keeping state on this? // When the user chooses two emojis, and the bot removes the second one, the event handler is called. We // need to make sure that the user initiated this removal, which we can do by making sure that the player // we're removing from the set has the same team as the emoji maps to. // TODO: We may want to make this a dictionary of IDs to Players to make this operation efficient. We could // use ContainsKey to do the contains checks efficiently. await this.GetTournamentsManager(guildChannel.Guild).DoReadWriteActionOnCurrentTournamentForMemberAsync( guildUser, async currentTournament => { Player player = this.GetPlayerFromReactionEventOrNull( currentTournament, guildUser, cachedMessage.Id, reaction.Emote.Name, out string errorMessage); if (player == null) { // User should have already received an error message from the add if it was relevant. this.Logger.Verbose( "Reaction addition ignored because it wasn't by a player. Message: {errorMessage}", errorMessage); return; } if (currentTournament.TryGetPlayerTeam(guildUser.Id, out Team storedTeam) && storedTeam == player.Team && currentTournament.TryRemovePlayer(guildUser.Id)) { this.Logger.Debug("Player {id} left team {name}", player.Id, player.Team.Name); await guildUser.SendMessageAsync( BotStrings.YouHaveLeftTeam(player.Team.Name), options: RequestOptionsSettings.Default); } }); }
public Task GoBackAsync() { return(this.DoReadWriteActionOnCurrentTournamentAsync( async currentTournament => { bool sendMessage = true; switch (currentTournament.Stage) { case TournamentStage.SetRoundRobins: currentTournament.TryClearReaders(); break; case TournamentStage.AddTeams: currentTournament.RoundRobinsCount = 0; break; case TournamentStage.AddPlayers: currentTournament.TryClearTeams(); currentTournament.ClearSymbolsToTeam(); List <Task <IMessage> > getJoinTeamMessagesTasks = new List <Task <IMessage> >(); foreach (ulong id in currentTournament.JoinTeamMessageIds) { getJoinTeamMessagesTasks.Add(this.Context.Channel.GetMessageAsync(id)); } IMessage[] joinTeamMessages = await Task.WhenAll(getJoinTeamMessagesTasks); await Task.WhenAll(joinTeamMessages .Select(message => this.Context.Channel.DeleteMessageAsync(message))); currentTournament.ClearJoinTeamMessageIds(); break; case TournamentStage.Rebracketing: // Don't send the message about the tournament starting again sendMessage = false; break; default: // Nothing to go back to, so do nothing. this.Logger.Debug("Could not go back on stage {stage}", currentTournament.Stage); await this.SendChannelMessage(BotStrings.CannotGoBack(currentTournament.Stage)); return; } TournamentStage previousStage = currentTournament.Stage == TournamentStage.Finals ? TournamentStage.RunningTournament : currentTournament.Stage - 1; await this.UpdateStageAsync(currentTournament, previousStage, sendMessage); })); }
public static async Task DoReadActionOnCurrentTournamentForMemberAsync( this TournamentsManager manager, IUser user, Func <IReadOnlyTournamentState, Task> action) { Result <Task> result = manager.TryReadActionOnCurrentTournament(action); if (result.Success) { await result.Value; return; } IDMChannel channel = await user.GetOrCreateDMChannelAsync(); await channel.SendMessageAsync(BotStrings.UnableToPerformCommand(result.ErrorMessage)); }
public async Task RemoveTournamentDirectorAsync(IGuildUser oldDirector, string tournamentName) { Verify.IsNotNull(oldDirector, nameof(oldDirector)); if (string.IsNullOrWhiteSpace(tournamentName)) { this.Logger.Debug("Couldn't remove director {id} for tournament with blank name", oldDirector.Id); return; } tournamentName = tournamentName.Trim(); TournamentsManager manager = this.GlobalManager.GetOrAdd(this.Context.Guild.Id, CreateTournamentsManager); // TODO: Harden this. Since it's not guaranteed to be the current tournament, we can't use the helper // methods if (!manager.TryGetTournament(tournamentName, out ITournamentState state)) { this.Logger.Debug( "Couldn't remove director {id} for nonexistent tournament {tournamentName}", oldDirector.Id, tournamentName); await this.Context.Channel.SendMessageAsync( BotStrings.TournamentDoesNotExist(tournamentName, this.Context.Guild.Name), options : RequestOptionsSettings.Default); return; } if (state.TryRemoveDirector(oldDirector.Id)) { this.Logger.Debug( "Removed {id} as a tournament director for {tournamentName}", oldDirector.Id, tournamentName); await this.Context.Channel.SendMessageAsync( BotStrings.RemovedTournamentDirector(tournamentName, this.Context.Guild.Name), options : RequestOptionsSettings.Default); return; } this.Logger.Debug( "User {id} is not a director for {tournamentName}, so could not be removed", oldDirector.Id, tournamentName); await this.Context.Channel.SendMessageAsync( BotStrings.UserNotTournamentDirector(tournamentName, this.Context.Guild.Name), options : RequestOptionsSettings.Default); }
public async Task RemovePlayerAsync(IGuildUser user) { Verify.IsNotNull(user, nameof(user)); bool removedSuccessfully = false; IEnumerable <ulong> teamRoleIds = null; await this.DoReadWriteActionOnCurrentTournamentAsync( currentTournament => { if (!currentTournament.TryRemovePlayer(user.Id)) { this.Logger.Debug("Player {id} wasn't on any team", user.Id); return(this.SendChannelMessage(BotStrings.PlayerIsNotOnAnyTeam(user.Mention))); } if (currentTournament.IsTorunamentInPlayStage()) { teamRoleIds = currentTournament.TournamentRoles.TeamRoleIds.Select(kvp => kvp.Value); } removedSuccessfully = true; return(Task.CompletedTask); }); if (!removedSuccessfully) { return; } // Get the team role from the player and remove it. if (teamRoleIds != null) { IEnumerable <IRole> teamRoles = user.RoleIds.Join( teamRoleIds, id => id, id => id, (userRoleId, teamId) => this.Context.Guild.GetRole(userRoleId)); await user.RemoveRolesAsync(teamRoles, RequestOptionsSettings.Default); } this.Logger.Debug( "Player {0} was removed from the tournament. Role removed: {2}", user.Id, teamRoleIds != null); await this.SendChannelMessage(BotStrings.PlayerRemoved(user.Mention)); }
public async Task GetScheduleAsync(string teamName = null) { // We may also want to expand schedule, so you show it just for a team, or for a particular round await this.DoReadActionOnCurrentTournamentAsync( currentTournament => { Team team = teamName == null ? null : new Team() { Name = teamName }; // TODO: Need to do more investigation on if this should be sent to the channel or to the user. return(this.Context.Channel.SendAllEmbeds( currentTournament.Schedule.Rounds, () => new EmbedBuilder() { Title = BotStrings.Schedule }, (round, roundIndex) => { EmbedFieldBuilder fieldBuilder = new EmbedFieldBuilder { Name = BotStrings.RoundNumber(roundIndex + 1) }; IEnumerable <Game> games = round.Games .Where(game => game.Teams != null && game.Reader != null); if (team != null) { games = games.Where(game => game.Teams.Contains(team)); } IEnumerable <string> lines = games .Select(game => BotStrings.ScheduleLine( game.Reader.Name, game.Teams.Select(team => team.Name).ToArray())); fieldBuilder.Value = string.Join("\n", lines); return fieldBuilder; })); } ); }
private async Task HandleSetRoundRobinsStage(ITournamentState currentTournament, SocketMessage message) { if (!int.TryParse(message.Content, out int rounds)) { this.Logger.Debug("Round robin count specified couldn't be parsed as an int"); return; } else if (rounds <= 0 || rounds > TournamentState.MaxRoundRobins) { this.Logger.Debug("Round robin count ({0}) specified is invalid", rounds); await message.Channel.SendMessageAsync( BotStrings.InvalidNumberOfRoundRobins(TournamentState.MaxRoundRobins), options : RequestOptionsSettings.Default); return; } currentTournament.RoundRobinsCount = rounds; await this.UpdateStage(currentTournament, TournamentStage.AddTeams, message.Channel); }
private static bool TryGetEmojis(int count, out IEmote[] emotes, out string errorMessage) { emotes = null; errorMessage = null; const int emojiLimit = 9 + 26; if (count < 0) { errorMessage = BotStrings.NumberOfTeamsMustBeGreaterThanZero; return(false); } else if (count > emojiLimit) { errorMessage = BotStrings.TooManyTeams(emojiLimit); return(false); } emotes = new IEmote[count]; int numberLoopLimit = Math.Min(9, count); // Encoding.Unicode.GetString(new byte[] { 49, 0, 227, 32 }) to Encoding.Unicode.GetString(new byte[] { 58, 0, 227, 32 }) is 1-9 byte[] numberBlock = { 49, 0, 227, 32 }; int i = 0; for (; i < numberLoopLimit; i++) { emotes[i] = new Emoji(Encoding.Unicode.GetString(numberBlock)); numberBlock[0] += 1; } // Encoding.Unicode.GetString(new byte[] { 60, 216, 230, 221 }) to Encoding.Unicode.GetString(new byte[] { 60, 216, 255, 221 }) is A-Z byte[] letterBlock = new byte[] { 60, 216, 230, 221 }; for (; i < count; i++) { emotes[i] = new Emoji(Encoding.Unicode.GetString(letterBlock)); letterBlock[2] += 1; } return(true); }
public Task AddTournamentDirectorAsync(IGuildUser newDirector, string tournamentName) { Verify.IsNotNull(newDirector, nameof(newDirector)); if (string.IsNullOrWhiteSpace(tournamentName)) { this.Logger.Debug("Did not add {id} to tournament with blank name", newDirector.Id); return(Task.CompletedTask); } TournamentsManager manager = this.GlobalManager.GetOrAdd(this.Context.Guild.Id, CreateTournamentsManager); ITournamentState state = new TournamentState(this.Context.Guild.Id, tournamentName.Trim()); bool updateSuccessful = state.TryAddDirector(newDirector.Id); manager.AddOrUpdateTournament( tournamentName, state, (name, tournamentState) => { updateSuccessful = tournamentState.TryAddDirector(newDirector.Id); return(tournamentState); }); // TODO: Need to handle this differently depending on the stage. Completed shouldn't do anything, and // after RoleSetup we should give them the TD role. if (updateSuccessful) { this.Logger.Debug( "Added {id} as a tournament director for {tournamentName}", newDirector.Id, tournamentName); return(this.SendChannelMessage( BotStrings.AddTournamentDirectorSuccessful(tournamentName, this.Context.Guild.Name))); } this.Logger.Debug( "{id} already a tournament director for {tournamentName}", newDirector.Id, tournamentName); return(this.SendChannelMessage( BotStrings.UserAlreadyTournamentDirector(tournamentName, this.Context.Guild.Name))); }
// TODO: Make string message and IGuildChannel? private async Task HandleAddTeamsStage(ITournamentState currentTournament, SocketMessage message) { if (!TeamsParser.TryParseTeams(message.Content, out IEnumerable <Team> teams, out string errorMessage)) { await message.Channel.SendMessageAsync(errorMessage, options : RequestOptionsSettings.Default); this.Logger.Debug("Team names could not be parsed. Error message: {errorMessage}", errorMessage); return; } currentTournament.AddTeams(teams); int teamsCount = currentTournament.Teams.Count(); if (teamsCount < 2) { await message.Channel.SendMessageAsync( BotStrings.MustBeTwoTeamsPerTournament, options : RequestOptionsSettings.Default); currentTournament.RemoveTeams(teams); this.Logger.Debug("Too few teams specified in AddTeams stage ({0})", teamsCount); return; } int maxTeamsCount = GetMaximumTeamCount(currentTournament); if (teamsCount > maxTeamsCount) { currentTournament.TryClearTeams(); await message.Channel.SendMessageAsync( BotStrings.TooManyTeams(maxTeamsCount), options : RequestOptionsSettings.Default); this.Logger.Debug("Too many teams specified in AddTeams stage ({0})", teamsCount); return; } if (!TryGetEmojis(teamsCount, out IEmote[] emotes, out errorMessage))
public async Task SwitchReaderAsync(IGuildUser oldReaderUser, IGuildUser newReaderUser) { Verify.IsNotNull(oldReaderUser, nameof(oldReaderUser)); Verify.IsNotNull(newReaderUser, nameof(newReaderUser)); bool switchSuccessful = false; ulong oldReaderRoleId = 0; await this.DoReadWriteActionOnCurrentTournamentAsync( async currentTournament => { // Only allow this after the tournament has started running if (!currentTournament.IsTorunamentInPlayStage()) { await this.SendChannelMessage(BotStrings.CommandOnlyUsedWhileTournamentRunning); return; } oldReaderRoleId = currentTournament.TournamentRoles.ReaderRoomRoleIds.Select(kvp => kvp.Value) .Join(oldReaderUser.RoleIds, id => id, id => id, (roomRoleId, readerRoleId) => roomRoleId) .FirstOrDefault(); if (oldReaderRoleId == default(ulong)) { await this.SendChannelMessage(BotStrings.CouldntGetRoleForTheOldReader); return; } if (!currentTournament.IsReader(oldReaderUser.Id)) { await this.SendChannelMessage(BotStrings.NotACurrentReader(oldReaderUser.Mention)); return; } else if (currentTournament.IsReader(newReaderUser.Id)) { await this.SendChannelMessage(BotStrings.IsAlreadyReader(newReaderUser.Mention)); return; } if (!currentTournament.TryRemoveReader(oldReaderUser.Id)) { await this.SendChannelMessage(BotStrings.UnknownErrorRemovingOldReader); return; } Reader newReader = new Reader() { Id = newReaderUser.Id, Name = newReaderUser.Nickname ?? newReaderUser.Username }; currentTournament.AddReaders(new Reader[] { newReader }); switchSuccessful = true; }); if (!switchSuccessful) { return; } IRole oldReaderRole = this.Context.Guild.GetRole(oldReaderRoleId); List <Task> roleChangeTasks = new List <Task> { newReaderUser.AddRoleAsync(oldReaderRole, RequestOptionsSettings.Default), oldReaderUser.RemoveRoleAsync(oldReaderRole, RequestOptionsSettings.Default) }; await Task.WhenAll(roleChangeTasks); await this.SendChannelMessage(BotStrings.ReadersSwitchedSuccessfully); }
public async Task AddPlayerAsync(IGuildUser user, string teamName) { Verify.IsNotNull(user, nameof(user)); bool addPlayerSuccessful = false; ulong teamRoleId = default(ulong); await this.DoReadWriteActionOnCurrentTournamentAsync( currentTournament => { // Rewrite exit-early, then choose whether we add the role or not if (!currentTournament.TryGetTeamFromName(teamName, out Team team)) { this.Logger.Debug("Player {id} could not be added to nonexistent {teamName}", user.Id, teamName); return(this.SendChannelMessage(BotStrings.TeamDoesNotExist(teamName))); } Player player = new Player() { Id = user.Id, Team = team }; if (!currentTournament.TryAddPlayer(player)) { this.Logger.Debug("Player {id} already on team; not added to {teamName}", user.Id, teamName); return(this.SendChannelMessage(BotStrings.PlayerIsAlreadyOnTeam(user.Mention))); } if (!currentTournament.IsTorunamentInPlayStage()) { addPlayerSuccessful = true; return(Task.CompletedTask); } KeyValuePair <Team, ulong> teamRoleIdPair = currentTournament.TournamentRoles.TeamRoleIds .FirstOrDefault(kvp => kvp.Key == team); if (teamRoleIdPair.Key != team) { // We can't add the role to the player. So undo adding the player to the tournament this.Logger.Debug( "Player {id} could not be added because role for {teamName} does not exist", user.Id, teamName); currentTournament.TryRemovePlayer(player.Id); return(this.SendChannelMessage( BotStrings.PlayerCannotBeAddedToTeamWithNoRole(user.Mention, teamName))); } teamRoleId = teamRoleIdPair.Value; addPlayerSuccessful = true; return(Task.CompletedTask); }); if (!addPlayerSuccessful) { return; } IRole teamRole = null; if (teamRoleId != default(ulong)) { teamRole = this.Context.Guild.GetRole(teamRoleId); await user.AddRoleAsync(teamRole, RequestOptionsSettings.Default); } this.Logger.Debug( "Player {0} successfully added to team {1}. Role added: {2}", user.Id, teamName, teamRole != null); await this.SendChannelMessage(BotStrings.AddPlayerSuccessful(user.Mention, teamName)); }
public async Task SetupFinalsAsync(IGuildUser readerUser, string rawTeamNameParts) { ITextChannel channel = null; await this.DoReadWriteActionOnCurrentTournamentAsync( async currentTournament => { if (currentTournament?.Stage != TournamentStage.RunningTournament) { this.Logger.Debug("Could not start finals in stage {stage}", currentTournament?.Stage); await this.SendChannelMessage(BotStrings.ErrorFinalsOnlySetDuringPrelimsOrPlayoffs); return; } if (!currentTournament.TryGetReader(readerUser.Id, out Reader reader)) { this.Logger.Debug("Could not start finals because {1} is not a reader", readerUser.Id); await this.SendChannelMessage(BotStrings.ErrorGivenUserIsntAReader); return; } if (rawTeamNameParts == null) { this.Logger.Debug( "Could not start finals because no teams were specified"); await this.SendChannelMessage(BotStrings.ErrorNoTeamsSpecified); return; } string combinedTeamNames = string.Join(" ", rawTeamNameParts).Trim(); if (!TeamNameParser.TryGetTeamNamesFromParts( combinedTeamNames, out IList <string> teamNames, out string errorMessage)) { this.Logger.Debug( "Could not start finals because of this error: {errorMessage}", errorMessage); await this.SendChannelMessage(BotStrings.ErrorGenericMessage(errorMessage)); return; } if (teamNames.Count != 2) { this.Logger.Debug( "Could not start finals because {count} teams were specified", teamNames.Count); await this.SendChannelMessage(BotStrings.ErrorTwoTeamsMustBeSpecifiedFinals(teamNames.Count)); return; } Team[] teams = teamNames.Select(name => new Team() { Name = name }) .ToArray(); if (currentTournament.Teams.Intersect(teams).Count() != teams.Length) { this.Logger.Debug( "Could not start finals because some teams were not in the tournament", teamNames.Count); await this.SendChannelMessage( BotStrings.ErrorAtLeastOneTeamNotInTournament(string.Join(", ", teamNames))); return; } // Create a finals channel and give access to the teams and readers Game finalsGame = new Game() { Reader = reader, Teams = teams }; int finalsRoundNumber = currentTournament.Schedule.Rounds.Count + 1; IList <Game> finalGames = currentTournament.Schedule.Rounds[currentTournament.Schedule.Rounds.Count - 1].Games; int roomIndex = 0; foreach (Game game in finalGames) { if (game.Reader.Equals(reader)) { break; } roomIndex++; } if (roomIndex >= finalGames.Count) { // Need to have a fall-back somehow. For now default to the first room. roomIndex = 0; } // TODO: Look into creating the channels after the update stage so we can release the lock // sooner. However, this does mean that a failure to create channels will leave us in a bad // state. channel = await this.ChannelManager.CreateChannelsForFinals( this.Context.Client.CurrentUser, currentTournament, finalsGame, finalsRoundNumber, roomIndex); currentTournament.UpdateStage( TournamentStage.Finals, out string nextStageTitle, out string nextStageInstructions); }); if (channel != null) { this.Logger.Debug("Finals started successfully"); await this.Context.Channel.SendMessageAsync( BotStrings.FinalsParticipantsPleaseJoin(channel.Mention), options : RequestOptionsSettings.Default); } }
internal async Task HandleOnReactionAdded( Cacheable <IUserMessage, ulong> cachedMessage, ISocketMessageChannel messageChannel, SocketReaction reaction) { // TODO: Combine checks for reaction add/remove handler into one method if (!cachedMessage.HasValue || reaction.UserId == this.Client.CurrentUser.Id) { // Ignore the bot's own additions. return; } if (!(reaction.Channel is IGuildChannel guildChannel)) { // Only listen to guild reacts. this.Logger.Verbose("Reaction addition ignored because it wasn't on a guild channel"); return; } if (!(reaction.User.IsSpecified && reaction.User.Value is IGuildUser guildUser)) { this.Logger.Verbose("Reaction addition ignored because it wasn't by a guild user"); return; } IUserMessage message = cachedMessage.Value; Player player = null; bool playerAdded = false; string errorMessage = null; bool attempt = this .GetTournamentsManager(guildChannel.Guild) .TryReadWriteActionOnCurrentTournament(currentTournament => { player = this.GetPlayerFromReactionEventOrNull( currentTournament, guildUser, message.Id, reaction.Emote.Name, out errorMessage); if (player == null) { // TODO: we may want to remove the reaction if it's on our team-join messages. this.Logger.Verbose( "Reaction addition ignored because it wasn't by a player. Message: {errorMessage}", errorMessage); return; } // Because player equality/hashing is only based on the ID, we can check if the player is in the set with // the new instance. playerAdded = currentTournament.TryAddPlayer(player); }); if (!(attempt && playerAdded) && errorMessage != null) { // TODO: We should remove the reaction they gave instead of this one (this may also require the manage // emojis permission?). This would also require a map from userIds/Players to emojis in the tournament // state. The Reactions collection doesn't have information on who added it, and iterating through each // emoji to see if the user was there would be slow. Task deleteReactionTask = message.RemoveReactionAsync( reaction.Emote, guildUser, RequestOptionsSettings.Default); Task sendMessageTask = guildUser.SendMessageAsync(errorMessage); await Task.WhenAll(deleteReactionTask, sendMessageTask); this.Logger.Verbose("Reaction removed. Message: {errorMessage}", errorMessage); return; } else if (player != null) { this.Logger.Debug("Player {id} joined team {name}", player.Id, player.Team.Name); await guildUser.SendMessageAsync(BotStrings.YouHaveJoinedTeam(player.Team.Name)); } }