Skip to content

697#744

Open
Arshadul-Monir wants to merge 3 commits intomainfrom
697
Open

697#744
Arshadul-Monir wants to merge 3 commits intomainfrom
697

Conversation

@Arshadul-Monir
Copy link
Collaborator

@Arshadul-Monir Arshadul-Monir commented Feb 7, 2026

697

Description of changes

Checklist before review

  • I have done a thorough self-review of the PR
  • Copilot has reviewed my latest changes, and all comments have been fixed and/or closed.
  • If I have made database changes, I have made sure I followed all the db repo rules listed in the wiki here. (check if no db changes)
  • All tests have passed
  • I have successfully deployed this PR to staging
  • I have done manual QA in both dev (and staging if possible) and attached screenshots below.

Screenshots

Dev

Staging

@github-actions
Copy link
Contributor

github-actions bot commented Feb 7, 2026

Available PR Commands

  • /ai - Triggers all AI review commands at once
  • /review - AI review of the PR changes
  • /describe - AI-powered description of the PR
  • /improve - AI-powered suggestions
  • /deploy - Deploy to staging

See: https://github.com/tahminator/codebloom/wiki/CI-Commands

@github-actions
Copy link
Contributor

github-actions bot commented Feb 7, 2026

Title

697


PR Type

Enhancement


Description

  • Add /leaderboard Discord slash command

  • Post weekly leaderboard embed per guild

  • Lookup DiscordClub by guildId in repo

  • Register command on bot ready event


Diagram Walkthrough

flowchart 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"]
Loading

File Walkthrough

Relevant files
Enhancement
DiscordClubManager.java
Guild-scoped weekly leaderboard embed sender                         

src/main/java/org/patinanetwork/codebloom/common/components/DiscordClubManager.java

  • Add method to send leaderboard by guildId.
  • Fetch club, leaderboard, and top users.
  • Build description with time remaining and links.
  • Send embed to configured leaderboard channel.
+89/-0   
DiscordClubRepository.java
Repository interface adds guildId lookup                                 

src/main/java/org/patinanetwork/codebloom/common/db/repos/discord/club/DiscordClubRepository.java

  • Add repository method getDiscordClubByGuildId.
  • Minor interface formatting updates.
+3/-0     
DiscordClubSqlRepository.java
SQL repo implements guildId-based club retrieval                 

src/main/java/org/patinanetwork/codebloom/common/db/repos/discord/club/DiscordClubSqlRepository.java

  • Implement getDiscordClubByGuildId with SQL join.
  • Map result to DiscordClub; handle absence.
  • Throw runtime on SQL errors.
+28/-0   
JDAEventListener.java
JDA listener adds /leaderboard command handling                   

src/main/java/org/patinanetwork/codebloom/jda/JDAEventListener.java

  • Inject DiscordClubManager into listener.
  • Register /leaderboard on ReadyEvent.
  • Handle /leaderboard by invoking manager.
+23/-0   

@github-actions
Copy link
Contributor

github-actions bot commented Feb 7, 2026

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Missing Rate Limit

The /leaderboard slash command has no per-guild 60-minute rate limiting and no ephemeral rate-limit response; add a per-guild cooldown store and reply ephemerally when throttled to meet AC.

@Override
public void onSlashCommandInteraction(final SlashCommandInteractionEvent event) {
    if (event.getName().equals("leaderboard")) {
        discordClubManager.sendWeeklyLeaderboardUpdateDiscordMessageForClub(event.getGuild().getId());
    }
}
Interaction Handling

The slash command handler does not acknowledge the interaction (reply/deferReply), risking "interaction failed" and offering no user feedback on success or failure; add event.deferReply(true) or appropriate ephemeral replies.

@Override
public void onSlashCommandInteraction(final SlashCommandInteractionEvent event) {
    if (event.getName().equals("leaderboard")) {
        discordClubManager.sendWeeklyLeaderboardUpdateDiscordMessageForClub(event.getGuild().getId());
    }
}
Possible Issue

getDiscordClubByGuildId(...).get() may throw if the guild is not configured; handle the empty Optional and provide a safe path (and user-visible ephemeral message) instead of throwing.

public void sendWeeklyLeaderboardUpdateDiscordMessageForClub(String guildId) {
    System.out.println("working");
    DiscordClub club = discordClubRepository.getDiscordClubByGuildId(guildId).get();
    var latestLeaderboard = leaderboardRepository.getRecentLeaderboardMetadata();

        LeaderboardFilterOptions options = LeaderboardFilterGenerator.builderWithTag(club.getTag())
                .page(1)
                .pageSize(5)

Comment on lines 31 to 42
@Override
public void onSlashCommandInteraction(final SlashCommandInteractionEvent event) {
if (event.getName().equals("leaderboard")) {
discordClubManager.sendWeeklyLeaderboardUpdateDiscordMessageForClub(event.getGuild().getId());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Suggested change
@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();
}

Comment on lines 251 to 324
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Suggested change
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;
}

Comment on lines 264 to 271
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Suggested change
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;

Arshadul-Monir and others added 3 commits February 7, 2026 19:14
697: Added repository function to get DiscordClub by guildId
…hat JDAEventListener takes JDA and registers it (so after initial JDA creation)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants