Conversation
Available PR Commands
See: https://github.com/tahminator/codebloom/wiki/CI-Commands |
Title697 PR TypeEnhancement Description
Diagram Walkthroughflowchart LR
A["JDA ReadyEvent"] --> B["Upsert /leaderboard command on all guilds"]
C["SlashCommandInteraction: /leaderboard"] --> D["DiscordClubManager.sendWeeklyLeaderboardUpdateDiscordMessageForClub(guildId)"]
D --> E["DiscordClubRepository.getDiscordClubByGuildId(guildId)"]
D --> F["LeaderboardRepository fetch latest + users"]
D --> G["Build embed: top 3, time left"]
G --> H["jdaClient.sendEmbedWithImages to club channel"]
|
| Relevant files | |||||||||
|---|---|---|---|---|---|---|---|---|---|
| Enhancement |
|
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
| @Override | ||
| public void onSlashCommandInteraction(final SlashCommandInteractionEvent event) { | ||
| if (event.getName().equals("leaderboard")) { | ||
| discordClubManager.sendWeeklyLeaderboardUpdateDiscordMessageForClub(event.getGuild().getId()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Suggestion: The slash command currently doesn't acknowledge the interaction, which will cause "This interaction failed" for users, and it lacks the required 60-minute per-server rate limit. Add a per-guild cooldown and send an ephemeral message when rate-limited. Also reply ephemerally on success, and guard against a null guild to prevent NPEs. [possible issue, importance: 8]
| @Override | |
| public void onSlashCommandInteraction(final SlashCommandInteractionEvent event) { | |
| if (event.getName().equals("leaderboard")) { | |
| discordClubManager.sendWeeklyLeaderboardUpdateDiscordMessageForClub(event.getGuild().getId()); | |
| } | |
| } | |
| private final java.util.Map<String, java.time.Instant> leaderboardCooldowns = new java.util.concurrent.ConcurrentHashMap<>(); | |
| @Override | |
| public void onSlashCommandInteraction(final SlashCommandInteractionEvent event) { | |
| if (!"leaderboard".equals(event.getName())) { | |
| return; | |
| } | |
| final var guild = event.getGuild(); | |
| if (guild == null) { | |
| event.reply("This command can only be used in a server.").setEphemeral(true).queue(); | |
| return; | |
| } | |
| final String guildId = guild.getId(); | |
| final java.time.Instant now = java.time.Instant.now(); | |
| final java.time.Instant last = leaderboardCooldowns.get(guildId); | |
| if (last != null && java.time.Duration.between(last, now).compareTo(java.time.Duration.ofMinutes(60)) < 0) { | |
| long remainingMinutes = 60 - java.time.Duration.between(last, now).toMinutes(); | |
| event.reply("This command is rate-limited. Try again in " + remainingMinutes + " minute(s).") | |
| .setEphemeral(true) | |
| .queue(); | |
| return; | |
| } | |
| leaderboardCooldowns.put(guildId, now); | |
| discordClubManager.sendWeeklyLeaderboardUpdateDiscordMessageForClub(guildId); | |
| event.reply("Posted the current weekly leaderboard to the configured channel.") | |
| .setEphemeral(true) | |
| .queue(); | |
| } |
| DiscordClub club = discordClubRepository.getDiscordClubByGuildId(guildId).get(); | ||
| var latestLeaderboard = leaderboardRepository.getRecentLeaderboardMetadata(); | ||
|
|
||
| LeaderboardFilterOptions options = LeaderboardFilterGenerator.builderWithTag(club.getTag()) | ||
| .page(1) | ||
| .pageSize(5) | ||
| .build(); | ||
|
|
||
| List<UserWithScore> users = LeaderboardUtils.filterUsersWithPoints( | ||
| leaderboardRepository.getLeaderboardUsersById(latestLeaderboard.getId(), options)); | ||
|
|
||
| Leaderboard currentLeaderboard = leaderboardRepository.getRecentLeaderboardMetadata(); | ||
|
|
||
| LocalDateTime shouldExpireByTime = Optional.ofNullable(currentLeaderboard.getShouldExpireBy()) | ||
| .orElse(StandardizedLocalDateTime.now()); | ||
|
|
||
| Duration remaining = Duration.between(StandardizedLocalDateTime.now(), shouldExpireByTime); | ||
|
|
||
| long daysLeft = remaining.toDays(); | ||
| long hoursLeft = remaining.toHours() % 24; | ||
| long minutesLeft = remaining.toMinutes() % 60; | ||
|
|
||
| String description = String.format( | ||
| """ | ||
| Dear %s users, | ||
|
|
||
| Here is a weekly update on the LeetCode leaderboard for our very own members! | ||
|
|
||
| 🥇- <@%s> - %s pts | ||
| 🥈- <@%s> - %s pts | ||
| 🥉- <@%s> - %s pts | ||
|
|
||
| To view the rest of the members, visit the website or check out the image embedded in this message! | ||
|
|
||
| Just as a reminder, there's %d day(s), %d hour(s), and %d minute(s) left until the leaderboard closes, so keep grinding! | ||
|
|
||
| View the full leaderboard for %s users at https://codebloom.patinanetwork.org/leaderboard?%s=true | ||
|
|
||
|
|
||
| See you next week! | ||
|
|
||
| Beep boop, | ||
| Codebloom | ||
| <%s> | ||
| """, | ||
| club.getName(), | ||
| getUser(users, 0).map(UserWithScore::getDiscordId).orElse("N/A"), | ||
| getUser(users, 0) | ||
| .map(UserWithScore::getTotalScore) | ||
| .map(String::valueOf) | ||
| .orElse("N/A"), | ||
| getUser(users, 1).map(UserWithScore::getDiscordId).orElse("N/A"), | ||
| getUser(users, 1) | ||
| .map(UserWithScore::getTotalScore) | ||
| .map(String::valueOf) | ||
| .orElse("N/A"), | ||
| getUser(users, 2).map(UserWithScore::getDiscordId).orElse("N/A"), | ||
| getUser(users, 2) | ||
| .map(UserWithScore::getTotalScore) | ||
| .map(String::valueOf) | ||
| .orElse("N/A"), | ||
| daysLeft, | ||
| hoursLeft, | ||
| minutesLeft, | ||
| club.getName(), | ||
| club.getTag().name().toLowerCase(), | ||
| serverUrlUtils.getUrl()); | ||
|
|
||
| var channelId = club.getDiscordClubMetadata().flatMap(DiscordClubMetadata::getLeaderboardChannelId); | ||
|
|
||
| if (guildId.isEmpty() || channelId.isEmpty()) { | ||
| log.error("club {} is skipped because of missing metadata", club.getName()); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Suggestion: Calling get() on an empty Optional will crash if the guild isn't registered, and the metadata check should only validate the channel ID. Safely handle the absent club by logging and returning early, and remove the unnecessary guildId.isEmpty() check. This prevents a runtime exception and avoids misleading skip conditions. [possible issue, importance: 9]
| DiscordClub club = discordClubRepository.getDiscordClubByGuildId(guildId).get(); | |
| var latestLeaderboard = leaderboardRepository.getRecentLeaderboardMetadata(); | |
| LeaderboardFilterOptions options = LeaderboardFilterGenerator.builderWithTag(club.getTag()) | |
| .page(1) | |
| .pageSize(5) | |
| .build(); | |
| List<UserWithScore> users = LeaderboardUtils.filterUsersWithPoints( | |
| leaderboardRepository.getLeaderboardUsersById(latestLeaderboard.getId(), options)); | |
| Leaderboard currentLeaderboard = leaderboardRepository.getRecentLeaderboardMetadata(); | |
| LocalDateTime shouldExpireByTime = Optional.ofNullable(currentLeaderboard.getShouldExpireBy()) | |
| .orElse(StandardizedLocalDateTime.now()); | |
| Duration remaining = Duration.between(StandardizedLocalDateTime.now(), shouldExpireByTime); | |
| long daysLeft = remaining.toDays(); | |
| long hoursLeft = remaining.toHours() % 24; | |
| long minutesLeft = remaining.toMinutes() % 60; | |
| String description = String.format( | |
| """ | |
| Dear %s users, | |
| Here is a weekly update on the LeetCode leaderboard for our very own members! | |
| 🥇- <@%s> - %s pts | |
| 🥈- <@%s> - %s pts | |
| 🥉- <@%s> - %s pts | |
| To view the rest of the members, visit the website or check out the image embedded in this message! | |
| Just as a reminder, there's %d day(s), %d hour(s), and %d minute(s) left until the leaderboard closes, so keep grinding! | |
| View the full leaderboard for %s users at https://codebloom.patinanetwork.org/leaderboard?%s=true | |
| See you next week! | |
| Beep boop, | |
| Codebloom | |
| <%s> | |
| """, | |
| club.getName(), | |
| getUser(users, 0).map(UserWithScore::getDiscordId).orElse("N/A"), | |
| getUser(users, 0) | |
| .map(UserWithScore::getTotalScore) | |
| .map(String::valueOf) | |
| .orElse("N/A"), | |
| getUser(users, 1).map(UserWithScore::getDiscordId).orElse("N/A"), | |
| getUser(users, 1) | |
| .map(UserWithScore::getTotalScore) | |
| .map(String::valueOf) | |
| .orElse("N/A"), | |
| getUser(users, 2).map(UserWithScore::getDiscordId).orElse("N/A"), | |
| getUser(users, 2) | |
| .map(UserWithScore::getTotalScore) | |
| .map(String::valueOf) | |
| .orElse("N/A"), | |
| daysLeft, | |
| hoursLeft, | |
| minutesLeft, | |
| club.getName(), | |
| club.getTag().name().toLowerCase(), | |
| serverUrlUtils.getUrl()); | |
| var channelId = club.getDiscordClubMetadata().flatMap(DiscordClubMetadata::getLeaderboardChannelId); | |
| if (guildId.isEmpty() || channelId.isEmpty()) { | |
| log.error("club {} is skipped because of missing metadata", club.getName()); | |
| return; | |
| } | |
| var clubOpt = discordClubRepository.getDiscordClubByGuildId(guildId); | |
| if (clubOpt.isEmpty()) { | |
| log.warn("No DiscordClub found for guildId {}", guildId); | |
| return; | |
| } | |
| DiscordClub club = clubOpt.get(); | |
| var latestLeaderboard = leaderboardRepository.getRecentLeaderboardMetadata(); | |
| ... | |
| var channelId = club.getDiscordClubMetadata().flatMap(DiscordClubMetadata::getLeaderboardChannelId); | |
| if (channelId.isEmpty()) { | |
| log.error("club {} is skipped because of missing metadata (leaderboard channel id)", club.getName()); | |
| return; | |
| } |
| LocalDateTime shouldExpireByTime = Optional.ofNullable(currentLeaderboard.getShouldExpireBy()) | ||
| .orElse(StandardizedLocalDateTime.now()); | ||
|
|
||
| Duration remaining = Duration.between(StandardizedLocalDateTime.now(), shouldExpireByTime); | ||
|
|
||
| long daysLeft = remaining.toDays(); | ||
| long hoursLeft = remaining.toHours() % 24; | ||
| long minutesLeft = remaining.toMinutes() % 60; |
There was a problem hiding this comment.
Suggestion: If the leaderboard has already expired, remaining becomes negative and shows negative days/hours/minutes in the message. Clamp the duration to zero before computing the time components. This ensures user-facing text is always non-negative and readable. [general, importance: 6]
| LocalDateTime shouldExpireByTime = Optional.ofNullable(currentLeaderboard.getShouldExpireBy()) | |
| .orElse(StandardizedLocalDateTime.now()); | |
| Duration remaining = Duration.between(StandardizedLocalDateTime.now(), shouldExpireByTime); | |
| long daysLeft = remaining.toDays(); | |
| long hoursLeft = remaining.toHours() % 24; | |
| long minutesLeft = remaining.toMinutes() % 60; | |
| LocalDateTime shouldExpireByTime = Optional.ofNullable(currentLeaderboard.getShouldExpireBy()) | |
| .orElse(StandardizedLocalDateTime.now()); | |
| Duration remaining = Duration.between(StandardizedLocalDateTime.now(), shouldExpireByTime); | |
| if (remaining.isNegative()) { | |
| remaining = Duration.ZERO; | |
| } | |
| long daysLeft = remaining.toDays(); | |
| long hoursLeft = remaining.toHours() % 24; | |
| long minutesLeft = remaining.toMinutes() % 60; |
697: Added repository function to get DiscordClub by guildId
…hat JDAEventListener takes JDA and registers it (so after initial JDA creation)
697
Description of changes
Checklist before review
Screenshots
Dev
Staging