From 75d41a6366b37c66798ee83d190266f53e7bdfae Mon Sep 17 00:00:00 2001 From: yHSJ Date: Tue, 17 Dec 2024 11:24:54 -0500 Subject: [PATCH] feat: match history command --- app/src/main/java/fi/sundae/bot/Bot.java | 5 +- .../sundae/bot/api/MatchResultSerializer.java | 14 ++ .../bot/commands/MatchHistoryCommand.java | 137 ++++++++++++++++++ .../bot/tournament/MatchResultEmbed.java | 96 ++++++++++++ 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/fi/sundae/bot/api/MatchResultSerializer.java create mode 100644 app/src/main/java/fi/sundae/bot/commands/MatchHistoryCommand.java create mode 100644 app/src/main/java/fi/sundae/bot/tournament/MatchResultEmbed.java diff --git a/app/src/main/java/fi/sundae/bot/Bot.java b/app/src/main/java/fi/sundae/bot/Bot.java index ef9561b..7c20515 100644 --- a/app/src/main/java/fi/sundae/bot/Bot.java +++ b/app/src/main/java/fi/sundae/bot/Bot.java @@ -1,6 +1,7 @@ package fi.sundae.bot; import com.jagrosh.jdautilities.command.CommandClientBuilder; +import fi.sundae.bot.commands.MatchHistoryCommand; import fi.sundae.bot.commands.MatchmakeCommand; import fi.sundae.bot.commands.ReadyCommand; import fi.sundae.bot.tournament.Match; @@ -31,7 +32,9 @@ public Bot(String token, String ownerId, String channelId, String adminRoleId) { .setOwnerId(ownerId) .setStatus(OnlineStatus.ONLINE) .addSlashCommands( - new ReadyCommand(MATCHMAKER), new MatchmakeCommand(adminRoleId, MATCHMAKER)); + new ReadyCommand(MATCHMAKER), + new MatchmakeCommand(adminRoleId, MATCHMAKER), + new MatchHistoryCommand(channelId, adminRoleId)); jdaBuilder.addEventListeners(commandBuilder.build(), new Listener(MATCHMAKER)); JDA jda = jdaBuilder.build(); diff --git a/app/src/main/java/fi/sundae/bot/api/MatchResultSerializer.java b/app/src/main/java/fi/sundae/bot/api/MatchResultSerializer.java new file mode 100644 index 0000000..78d5264 --- /dev/null +++ b/app/src/main/java/fi/sundae/bot/api/MatchResultSerializer.java @@ -0,0 +1,14 @@ +package fi.sundae.bot.api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; + +public class MatchResultSerializer implements JsonSerializer { + @Override + public JsonElement serialize(MatchResult src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getSerializedName()); + } +} diff --git a/app/src/main/java/fi/sundae/bot/commands/MatchHistoryCommand.java b/app/src/main/java/fi/sundae/bot/commands/MatchHistoryCommand.java new file mode 100644 index 0000000..9e2d307 --- /dev/null +++ b/app/src/main/java/fi/sundae/bot/commands/MatchHistoryCommand.java @@ -0,0 +1,137 @@ +package fi.sundae.bot.commands; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.jagrosh.jdautilities.command.SlashCommand; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import fi.sundae.bot.api.MatchResult; +import fi.sundae.bot.api.MatchResultSerializer; +import fi.sundae.bot.tournament.MatchResultEmbed; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MatchHistoryCommand extends SlashCommand { + + private final String CHANNEL_ID; + private final String ADMIN_ROLE_ID; + private final Logger LOGGER = LoggerFactory.getLogger(MatchHistoryCommand.class); + + public MatchHistoryCommand(String channelId, String adminRoleId) { + this.CHANNEL_ID = channelId; + this.ADMIN_ROLE_ID = adminRoleId; + this.name = "match_history"; + this.help = "View parsed match history in JSON"; + } + + @Override + protected void execute(SlashCommandEvent event) { + event.deferReply(true).queue(); + if (Objects.requireNonNull(event.getMember()).getRoles().stream() + .noneMatch(role -> role.getId().equals(ADMIN_ROLE_ID))) { + event.getHook().editOriginalEmbeds(getNotAllowedEmbed()).queue(); + return; + } + + TextChannel channel = event.getJDA().getTextChannelById(CHANNEL_ID); + fetchAllMessages(channel) + .thenAccept( + messages -> { + List matchResults = new ArrayList<>(); + for (Message message : messages) { + if (!message.getContentRaw().isEmpty() || message.getEmbeds().isEmpty()) { + continue; + } + + MessageEmbed embed = message.getEmbeds().get(0); + matchResults.add(new MatchResultEmbed(embed)); + } + + Gson gson = + new GsonBuilder() + .registerTypeAdapter(MatchResult.class, new MatchResultSerializer()) + .create(); + + event.getHook().editOriginal("```" + gson.toJson(matchResults) + "```").queue(); + }); + } + + public CompletableFuture> fetchAllMessages(TextChannel channel) { + CompletableFuture> future = new CompletableFuture<>(); + List messages = new ArrayList<>(); + + fetchMessagesBefore(channel, null, messages, future); + return future; + } + + private void fetchMessagesBefore( + TextChannel channel, + String lastMessageId, + List messages, + CompletableFuture> future) { + int fetchLimit = 100; // limit imposed by Discord + if (lastMessageId == null) { + channel + .getHistory() + .retrievePast(fetchLimit) + .queue( + retrievedMessages -> { + List filteredMessages = + retrievedMessages.stream() + .filter( + message -> channel.getJDA().getSelfUser().equals(message.getAuthor())) + .collect(Collectors.toCollection(ArrayList::new)); + messages.addAll(filteredMessages); + + if (filteredMessages.size() < fetchLimit) { + future.complete(messages); + } else { + String oldestMessageId = + retrievedMessages.get(retrievedMessages.size() - 1).getId(); + fetchMessagesBefore(channel, oldestMessageId, messages, future); + } + }, + error -> { + LOGGER.error("failed to fetch messages", error); + future.completeExceptionally(error); + }); + } else { + channel + .getHistoryBefore(lastMessageId, fetchLimit) + .queue( + history -> { + List retrievedMessages = history.getRetrievedHistory(); + messages.addAll(retrievedMessages); + + if (retrievedMessages.size() < fetchLimit) { + future.complete(messages); + } else { + String oldestMessageId = + retrievedMessages.get(retrievedMessages.size() - 1).getId(); + fetchMessagesBefore(channel, oldestMessageId, messages, future); + } + }, + error -> { + LOGGER.error("failed to fetch messages", error); + future.completeExceptionally(error); + }); + } + } + + private MessageEmbed getNotAllowedEmbed() { + return new EmbedBuilder() + .setColor(Color.RED) + .setTitle("Permission Denied") + .setDescription("Only a tournament admin can use this command.") + .build(); + } +} diff --git a/app/src/main/java/fi/sundae/bot/tournament/MatchResultEmbed.java b/app/src/main/java/fi/sundae/bot/tournament/MatchResultEmbed.java new file mode 100644 index 0000000..cca8cbd --- /dev/null +++ b/app/src/main/java/fi/sundae/bot/tournament/MatchResultEmbed.java @@ -0,0 +1,96 @@ +package fi.sundae.bot.tournament; + +import fi.sundae.bot.api.MatchResult; +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.dv8tion.jda.api.entities.MessageEmbed; + +public class MatchResultEmbed { + private String playerOne; + private String playerTwo; + private String nodeId; + private String gameId; + private int playerOneKills, playerTwoKills; + private MatchResult result; + + public MatchResultEmbed(MessageEmbed embed) { + Color embedColor = embed.getColor(); + if (Color.RED.equals(embedColor)) { + String title = Objects.requireNonNull(embed.getTitle()).toLowerCase(); + if (title.contains("disagreement")) result = MatchResult.DISAGREEMENT; + else if (title.contains("disconnect")) result = MatchResult.DISCONNECT; + else if (title.contains("timed out")) result = MatchResult.TIMEOUT; + + String description = Objects.requireNonNull(embed.getDescription()); + String regex = "<@(\\d+)>"; + + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(description); + List discordIds; + discordIds = new ArrayList<>(); + while (matcher.find()) { + discordIds.add(matcher.group(1)); + } + playerOne = discordIds.get(0); + playerTwo = discordIds.get(1); + } else result = MatchResult.FINISHED; + + List fields = embed.getFields(); + for (MessageEmbed.Field field : fields) { + if ("Player 1".equals(field.getName())) { + playerOne = Objects.requireNonNull(field.getValue()).replace("<@", "").replace(">", ""); + } else if ("Player 2".equals(field.getName())) { + playerTwo = Objects.requireNonNull(field.getValue()).replace("<@", "").replace(">", ""); + } else if ("Node ID".equals(field.getName())) { + nodeId = Objects.requireNonNull(field.getValue()).replace("`", ""); + } else if ("Game ID".equals(field.getName())) { + gameId = Objects.requireNonNull(field.getValue()).replace("`", ""); + } else if ("Kill Counts".equals(field.getName())) { + String regex = ":(\\s*\\d+)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(Objects.requireNonNull(field.getValue())); + + List numbers = new ArrayList<>(); + while (matcher.find()) { + int number = Integer.parseInt(matcher.group(1).trim()); + numbers.add(number); + } + + playerOneKills = numbers.get(0); + playerTwoKills = numbers.get(1); + } + } + } + + public String getPlayerOne() { + return playerOne; + } + + public String getPlayerTwo() { + return playerTwo; + } + + public String getNodeId() { + return nodeId; + } + + public String getGameId() { + return gameId; + } + + public int getPlayerOneKills() { + return playerOneKills; + } + + public int getPlayerTwoKills() { + return playerTwoKills; + } + + public MatchResult getResult() { + return result; + } +}