diff --git a/src/main/java/pw/chew/mlb/MLBBot.java b/src/main/java/pw/chew/mlb/MLBBot.java index 7ad7c6a..55f9d85 100644 --- a/src/main/java/pw/chew/mlb/MLBBot.java +++ b/src/main/java/pw/chew/mlb/MLBBot.java @@ -21,6 +21,7 @@ import pw.chew.mlb.commands.StandingsCommand; import pw.chew.mlb.commands.StartGameCommand; import pw.chew.mlb.commands.StopGameCommand; +import pw.chew.mlb.commands.TeamCommand; import pw.chew.mlb.listeners.InteractionHandler; import pw.chew.mlb.listeners.JDAListeners; import pw.chew.mlb.util.TeamEmoji; @@ -66,7 +67,7 @@ public static void main(String[] args) throws IOException { new StartGameCommand(), new StopGameCommand(), new ScoreCommand(), new SetInfoCommand(), new ConfigCommand(), new PlanGameCommand() , // Stats Commands - new StandingsCommand(), new GameInfoCommand() + new StandingsCommand(), new GameInfoCommand(), new TeamCommand() ); //client.forceGuildOnly("148195924567392257"); @@ -77,6 +78,7 @@ public static void main(String[] args) throws IOException { // Register JDA jda = JDABuilder.createDefault(prop.getProperty("token")) .setStatus(OnlineStatus.ONLINE) + .enableCache(CacheFlag.SCHEDULED_EVENTS) .setActivity(Activity.playing("Booting...")) .addEventListeners( waiter, commandClient // JDA-Chewtils stuff diff --git a/src/main/java/pw/chew/mlb/commands/StandingsCommand.java b/src/main/java/pw/chew/mlb/commands/StandingsCommand.java index 742adc8..5a038d5 100644 --- a/src/main/java/pw/chew/mlb/commands/StandingsCommand.java +++ b/src/main/java/pw/chew/mlb/commands/StandingsCommand.java @@ -3,6 +3,7 @@ import com.jagrosh.jdautilities.command.SlashCommand; import com.jagrosh.jdautilities.command.SlashCommandEvent; import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.interactions.DiscordLocale; import net.dv8tion.jda.api.interactions.InteractionContextType; import net.dv8tion.jda.api.interactions.commands.OptionType; @@ -42,8 +43,12 @@ public StandingsCommand() { protected void execute(SlashCommandEvent event) { String division = event.optString("division", "American League West"); + event.replyEmbeds(buildDivisionEmbed(division)).setEphemeral(true).queue(); + } + + public static MessageEmbed buildDivisionEmbed(String division) { // first we get standings - var standings = MLBAPIUtil.getStandings().get(division); + var standings = MLBAPIUtil.getStandings("103,104").get(division); List teams = new ArrayList<>(); for (MLBAPIUtil.Standing standing : standings) { @@ -64,6 +69,6 @@ protected void execute(SlashCommandEvent event) { .setTitle("Standings for " + division) .setDescription(String.join("\n", teams)); - event.replyEmbeds(embed.build()).setEphemeral(true).queue(); + return embed.build(); } } diff --git a/src/main/java/pw/chew/mlb/commands/TeamCommand.java b/src/main/java/pw/chew/mlb/commands/TeamCommand.java new file mode 100644 index 0000000..52d15f0 --- /dev/null +++ b/src/main/java/pw/chew/mlb/commands/TeamCommand.java @@ -0,0 +1,249 @@ +package pw.chew.mlb.commands; + +import com.jagrosh.jdautilities.command.SlashCommand; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; +import net.dv8tion.jda.api.interactions.InteractionContextType; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.selections.SelectOption; +import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu; +import org.json.JSONArray; +import org.json.JSONObject; +import pw.chew.chewbotcca.util.MiscUtil; +import pw.chew.chewbotcca.util.RestClient; +import pw.chew.mlb.util.AutocompleteUtil; +import pw.chew.mlb.util.MLBAPIUtil; +import pw.chew.mlb.util.TeamEmoji; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static pw.chew.mlb.MLBBot.SEASON; + +public class TeamCommand extends SlashCommand { + public TeamCommand() { + this.name = "team"; + this.help = "Get information about a team."; + this.options = List.of( + new OptionData(OptionType.STRING, "team", "Type to search for a team!", true, true) + ); + + // Installable everywhere, since info and always ephemeral + this.contexts = new InteractionContextType[]{ + InteractionContextType.GUILD, + InteractionContextType.BOT_DM, + InteractionContextType.PRIVATE_CHANNEL + }; + } + + @Override + protected void execute(SlashCommandEvent event) { + String teamId = event.optString("team", "140"); + List affiliates = MLBAPIUtil.getAffiliates(teamId); + MLBAPIUtil.Affiliate currentAffiliate = affiliates.stream().filter(af -> (af.id() + "").equals(teamId)).findFirst().orElse(null); + if (currentAffiliate == null) { + event.reply("I couldn't find a team with that ID!").setEphemeral(true).queue(); + return; + } + + String componentKeyBase = "team:" + teamId + ":"; + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("Information for " + currentAffiliate.name()) + .setDescription("Yeah this sure is some information, huh?") + .addField("Record", "%s-%s".formatted(currentAffiliate.wins(), currentAffiliate.losses()), true) + .setFooter("Team ID: " + teamId); + + // Build the affiliate list :3 + List options = new ArrayList<>(); + for (MLBAPIUtil.Affiliate affiliate : affiliates) { + options.add(SelectOption.of(affiliate.name(), affiliate.id() + "").withDescription(affiliate.sport().name()).withEmoji(TeamEmoji.fromName(affiliate.name()))); + } + + StringSelectMenu menu = StringSelectMenu.create(componentKeyBase + "affiliates") + .setPlaceholder("Select an Affiliate!") + .addOptions(options) + .build(); + + event.replyEmbeds(embed.build()) + .addActionRow(menu) + .addActionRow( + Button.primary(componentKeyBase + "standings:" + currentAffiliate.leagueId(), "Standings").withEmoji(Emoji.fromUnicode("📊")), + Button.primary(componentKeyBase + "roster", "Roster").withEmoji(Emoji.fromUnicode("📋")), + Button.primary(componentKeyBase + "transactions", "Transactions").withEmoji(Emoji.fromUnicode("💸")), + Button.primary(componentKeyBase + "schedule", "Schedule").withEmoji(Emoji.fromUnicode("📅")), + Button.primary(componentKeyBase + "stats", "Stats").withEmoji(Emoji.fromUnicode("📈")) + ).setEphemeral(true).queue(); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event) { + event.replyChoices(AutocompleteUtil.getTeams("1", event.getFocusedOption().getValue())).queue(); + } + + public static void handleButton(ButtonInteractionEvent event) { + // 0 => always "team", irrelevant + // 1 => always team ID + // 2 => action + // 3 => sub-action if applicable + String[] components = event.getComponentId().split(":"); + String teamId = components[1]; + String action = components[2]; + String subAction = components.length > 3 ? components[3] : null; + + switch (action) { + case "standings" -> event.getInteraction().editMessageEmbeds(handleStandingsEmbed(teamId)).queue(); + case "roster" -> event.getInteraction().editMessageEmbeds(handleRosterEmbed(teamId)).queue(); + case "transactions" -> event.getInteraction().editMessageEmbeds(handleTransactionsEmbed(teamId)).queue(); + case "schedule" -> event.getInteraction().editMessageEmbeds(handleScheduleEmbed(teamId)).queue(); + case "stats" -> event.getInteraction().editMessageEmbeds(handleStatsEmbed(teamId)).queue(); + } + } + + public static void handleSelect(StringSelectInteractionEvent event) {} + + private static List buildBaseActionRows(String teamId) { + return Collections.emptyList(); + } + + private static MessageEmbed handleStandingsEmbed(String teamId) { + return new EmbedBuilder() + .build(); + } + + private static MessageEmbed handleRosterEmbed(String teamId) { + // get roster + JSONObject roster = RestClient.get("https://statsapi.mlb.com/api/v1/teams/%s/roster?rosterType=depthChart&season=2025&fields=roster,person,primaryNumber,fullName,position,name,type,status,code,description" + .formatted(teamId)).asJSONObject(); + + Map> players = new LinkedHashMap<>(); + players.put("Starting Pitcher", new ArrayList<>()); + players.put("Infielder", new ArrayList<>()); + players.put("Outfielder", new ArrayList<>()); + players.put("Bullpen", new ArrayList<>()); + players.put("Catcher", new ArrayList<>()); + players.put("Designated Hitter", new ArrayList<>()); + + for (JSONObject player : MiscUtil.toList(roster.getJSONArray("roster"), JSONObject.class)) { + JSONObject position = player.getJSONObject("position"); + String name = player.getJSONObject("person").getString("fullName"); + switch (player.getJSONObject("status").getString("code")) { + case "D60": + case "RM": + // skip this player entirely + break; + case "D10": + case "D15": + // append injury emoji to end of name + name += " " + "+"; // TODO: Replace with actual injury emoji + case "A": + // time to get ready + switch (position.getString("type")) { + case "Catcher", "Infielder", "Outfielder" -> { + players.get(position.getString("type")).add(name); + } + case "Pitcher" -> { + if (position.getString("name").equals("Starting Pitcher")) { + players.get("Starting Pitcher").add(name); + } else { + players.get("Bullpen").add(name); + } + } + case "Hitter" -> { + players.get("Designated Hitter").add(name); + } + } + } + } + + + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("Roster") + .setDescription("Only shows active roster."); + + for (Map.Entry> entry : players.entrySet()) { + String position = entry.getKey(); + List playerList = entry.getValue(); + + if (playerList.isEmpty()) { + continue; + } + + StringBuilder sb = new StringBuilder(); + for (String player : playerList) { + sb.append(player).append("\n"); + } + + embed.addField(position + (position.equals("Bullpen") ? "" : "s"), sb.toString(), true); + } + + return embed.build(); + } + + private static MessageEmbed handleTransactionsEmbed(String teamId) { + return new EmbedBuilder() + .build(); + } + + private static MessageEmbed handleScheduleEmbed(String teamId) { + JSONObject data = RestClient.get("https://statsapi.mlb.com/api/v1/teams/%s?season=%s&hydrate=previousSchedule,nextSchedule" + .formatted(teamId, SEASON)).asJSONObject().getJSONArray("teams").getJSONObject(0); + + List games = new ArrayList<>(); + List gamePks = new ArrayList<>(); + JSONArray prevGames = data.getJSONObject("previousGameSchedule").getJSONArray("dates"); + JSONArray nextGames = data.getJSONObject("nextGameSchedule").getJSONArray("dates"); + + for (JSONArray ar : new JSONArray[]{prevGames, nextGames}) { + for (int i = 0; i < ar.length(); i++) { + JSONArray gamesForDate = ar.getJSONObject(i).getJSONArray("games"); + for (int j = 0; j < gamesForDate.length(); j++) { + gamePks.add(gamesForDate.getJSONObject(j).getInt("gamePk")); + } + } + } + + // now we get games cus mlb is silly :3 + JSONObject gameData = RestClient.get("https://statsapi.mlb.com/api/v1/schedule?language=en&gamePks=%s&useLatestGames=true&hydrate=team" + .formatted(gamePks.stream().distinct().map(String::valueOf).reduce((a, b) -> a + "," + b).orElse(""))).asJSONObject(); + + // now we do the wrapping + JSONArray gameDates = gameData.getJSONArray("dates"); + for (int i = 0; i < gameDates.length(); i++) { + JSONArray gamesForDate = gameDates.getJSONObject(i).getJSONArray("games"); + for (int j = 0; j < gamesForDate.length(); j++) { + games.add(new MLBAPIUtil.Game(gamesForDate.getJSONObject(j))); + } + } + + int teamIdInt = Integer.parseInt(teamId); + + StringBuilder res = new StringBuilder(); + for (MLBAPIUtil.Game game : games) { + //res.append(ScheduleCommand.gameToString(game, teamIdInt, -1)).append("\n"); + } + + return new EmbedBuilder() + .setTitle("Schedule for %s".formatted(data.getString("name"))) + .setDescription(res.toString()) + .build(); + } + + private static MessageEmbed handleStatsEmbed(String teamId) { + return new EmbedBuilder() + .build(); + } +} diff --git a/src/main/java/pw/chew/mlb/listeners/InteractionHandler.java b/src/main/java/pw/chew/mlb/listeners/InteractionHandler.java index d8ecf8f..81836f0 100644 --- a/src/main/java/pw/chew/mlb/listeners/InteractionHandler.java +++ b/src/main/java/pw/chew/mlb/listeners/InteractionHandler.java @@ -12,10 +12,20 @@ import pw.chew.mlb.objects.GameBlurb; import pw.chew.mlb.objects.GameState; import pw.chew.mlb.util.EmbedUtil; +import pw.chew.mlb.commands.TeamCommand; +import pw.chew.mlb.util.MLBAPIUtil; + +import java.util.ArrayList; +import java.util.List; public class InteractionHandler extends ListenerAdapter { @Override public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { + if (event.getComponentId().startsWith("team:")) { + TeamCommand.handleButton(event); + return; + } + if (event.getComponentId().startsWith("plangame:")) { String action = event.getComponentId().split(":")[1]; String gamePk = event.getComponentId().split(":")[2]; @@ -85,11 +95,9 @@ public void onStringSelectInteraction(StringSelectInteractionEvent event) { String type = parts[0]; // Future proofing - //noinspection SwitchStatementWithTooFewBranches switch (type) { - case "gameinfo" -> { - GameInfoCommand.handleSelectMenu(event); - } + case "gameinfo" -> GameInfoCommand.handleSelectMenu(event); + case "team" -> TeamCommand.handleSelect(event); } } } diff --git a/src/main/java/pw/chew/mlb/objects/MLBTeam.java b/src/main/java/pw/chew/mlb/objects/MLBTeam.java index 0f46153..36968bb 100644 --- a/src/main/java/pw/chew/mlb/objects/MLBTeam.java +++ b/src/main/java/pw/chew/mlb/objects/MLBTeam.java @@ -11,6 +11,14 @@ public String name() { return data.getJSONObject("team").getString("teamName"); } + public String clubName() { + return data.getJSONObject("team").getString("clubName"); + } + + public String abbreviation() { + return data.getJSONObject("team").getString("abbreviation"); + } + public JSONObject record() { return data.getJSONObject("leagueRecord"); } @@ -23,6 +31,10 @@ public int losses() { return record().getInt("losses"); } + public int score() { + return data.getInt("score"); + } + public String probablePitcher() { JSONObject fallback = new JSONObject().put("fullName", "TBD"); return data.optJSONObject("probablePitcher", fallback).getString("fullName"); diff --git a/src/main/java/pw/chew/mlb/util/MLBAPIUtil.java b/src/main/java/pw/chew/mlb/util/MLBAPIUtil.java index 0459dc3..e2335d9 100644 --- a/src/main/java/pw/chew/mlb/util/MLBAPIUtil.java +++ b/src/main/java/pw/chew/mlb/util/MLBAPIUtil.java @@ -7,17 +7,21 @@ import org.json.JSONArray; import org.json.JSONObject; import pw.chew.chewbotcca.util.RestClient; +import pw.chew.mlb.objects.MLBTeam; import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import static pw.chew.mlb.MLBBot.SEASON; -import static pw.chew.mlb.MLBBot.TEAMS; public class MLBAPIUtil { /// CACHING /// @@ -94,13 +98,26 @@ public static Map> getLineup(String gamePk, String homeAway return response; } + public static List getAffiliates(String teamId) { + JSONArray affiliates = RestClient.get("https://statsapi.mlb.com/api/v1/teams/affiliates?teamIds=%s&season=%s&hydrate=standings,sport&fields=teams,name,id,venue,name,league,division,sport,abbreviation,sortOrder,record,wins,losses,winningPercentage,active" + .formatted(teamId, SEASON)).asJSONObject().getJSONArray("teams"); + + // wrap each array item as an affiliate object + List affiliatesList = new ArrayList<>(); + for (int i = 0; i < affiliates.length(); i++) { + affiliatesList.add(new Affiliate(affiliates.getJSONObject(i))); + } + affiliatesList.sort(Comparator.comparingInt(Affiliate::sortOrder)); + return affiliatesList; + } + /** * Gets the current MLB standings. * * TODO: Support MiLB. */ - public static Map> getStandings() { - JSONArray standings = RestClient.get("https://statsapi.mlb.com/api/v1/standings?leagueId=103,104&hydrate=division&season=%s".formatted(SEASON)).asJSONObject().getJSONArray("records"); + public static Map> getStandings(String leagueId) { + JSONArray standings = RestClient.get("https://statsapi.mlb.com/api/v1/standings?leagueId=%s&hydrate=division&season=%s".formatted(leagueId, SEASON)).asJSONObject().getJSONArray("records"); HashMap> standingsMap = new HashMap<>(); for (int i = 0; i < standings.length(); i++) { JSONObject division = standings.getJSONObject(i); @@ -122,6 +139,31 @@ public static Map> getStandings() { } + + public static List getSchedule(int teamId, String sportId) { + JSONArray games = new JSONObject(RestClient.get("https://statsapi.mlb.com/api/v1/schedule?lang=en&sportId=%s&season=%s&teamId=%s&hydrate=team&fields=totalGames,totalGamesInProgress,dates,date,games,gamePk,gameType,gameDate,status,detailedState,abstractGameState,teams,away,home,team,name,id,clubName,abbreviation,score,seriesGameNumber,seriesDescription" + .formatted(sportId, SEASON, teamId))) + .getJSONArray("dates"); + + // iterate through date + List schedule = new ArrayList<>(); + for (int i = 0; i < games.length(); i++) { + // Formatted as YYYY-MM-DD + String date = games.getJSONObject(i).getString("date"); + + // iterate through games + JSONArray dayGames = games.getJSONObject(i).getJSONArray("games"); + for (int j = 0; j < dayGames.length(); j++) { + JSONObject game = dayGames.getJSONObject(j); + schedule.add(new Game(game)); + } + } + + return schedule; + } + + /// Records for data wrapping /// + public record Sports(JSONArray raw) { public List asChoices() { List choices = new ArrayList<>(); @@ -242,6 +284,70 @@ public String abbreviation() { } } + public record Affiliate(JSONObject raw) { + public int id() { + return raw.getInt("id"); + } + + public String name() { + return raw.getString("name"); + } + + public String venueName() { + return raw.getJSONObject("venue").getString("name"); + } + + public String abbreviation() { + return raw.getString("abbreviation"); + } + + public int leagueId() { + return raw.getJSONObject("league").getInt("id"); + } + + public String leagueName() { + return raw.getJSONObject("league").getString("name"); + } + + public String divisionName() { + return raw.getJSONObject("division").getString("name"); + } + + public Sport sport() { + return new Sport(raw.getJSONObject("sport")); + } + + public int wins() { + return raw.getJSONObject("record").getInt("wins"); + } + + public int losses() { + return raw.getJSONObject("record").getInt("losses"); + } + + public String pct() { + return raw.getJSONObject("record").getString("winningPercentage"); + } + + public boolean active() { + return raw.getBoolean("active"); + } + + public record Sport(JSONObject raw) { + public String name() { + return raw.getString("name"); + } + + public String abbreviation() { + return raw.getString("abbreviation"); + } + } + + public int sortOrder() { + return raw.getJSONObject("sport").getInt("sortOrder"); + } + } + public record Standing(JSONObject raw) { public String teamName() { // team > name @@ -304,4 +410,52 @@ public String lastTen() { return "%s-%s".formatted(lastTen.getInt("wins"), lastTen.getInt("losses")); } } + + public record Game(JSONObject raw) { + public String gamePk() { + return raw.getString("gamePk"); + } + + public String gameType() { + return raw.getString("gameType"); + } + + public OffsetDateTime gameDate() { + TemporalAccessor accessor = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(raw.getString("gameDate")); + return OffsetDateTime.from(accessor); + } + + public String abstractGameState() { + return raw.getJSONObject("status").getString("abstractGameState"); + } + + public String detailedState() { + return raw.getJSONObject("status").getString("detailedState"); + } + + public boolean isCancelled() { + // detailedState == "Cancelled" + return detailedState().equals("Cancelled"); + } + + public boolean isFinal() { + return abstractGameState().equals("Final"); + } + + public MLBTeam away() { + return new MLBTeam(raw.getJSONObject("teams").getJSONObject("away")); + } + + public MLBTeam home() { + return new MLBTeam(raw.getJSONObject("teams").getJSONObject("home")); + } + + public int seriesGameNumber() { + return raw.getInt("seriesGameNumber"); + } + + public String seriesDescription() { + return raw.getString("seriesDescription"); + } + } }