Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
import org.patinanetwork.codebloom.common.url.ServerUrlUtils;
import org.patinanetwork.codebloom.common.utils.leaderboard.LeaderboardUtils;
import org.patinanetwork.codebloom.jda.client.JDAClient;
import org.patinanetwork.codebloom.jda.client.options.EmbeddedDirectMessageOptions;
import org.patinanetwork.codebloom.jda.client.options.EmbeddedImagesMessageOptions;
import org.patinanetwork.codebloom.jda.properties.FeatureFlagConfiguration;
import org.springframework.stereotype.Component;

@Component
Expand All @@ -39,20 +41,23 @@ public class DiscordClubManager {
private final UserRepository userRepository;
private final ServerUrlUtils serverUrlUtils;
private final LeaderboardManager leaderboardManager;
private final FeatureFlagConfiguration featureFlagConfiguration;

public DiscordClubManager(
final ServerUrlUtils serverUrlUtils,
final JDAClient jdaClient,
final LeaderboardRepository leaderboardRepository,
final DiscordClubRepository discordClubRepository,
final UserRepository userRepository,
final LeaderboardManager leaderboardManager) {
final LeaderboardManager leaderboardManager,
final FeatureFlagConfiguration featureFlagConfiguration) {
this.serverUrlUtils = serverUrlUtils;
this.jdaClient = jdaClient;
this.leaderboardRepository = leaderboardRepository;
this.discordClubRepository = discordClubRepository;
this.userRepository = userRepository;
this.leaderboardManager = leaderboardManager;
this.featureFlagConfiguration = featureFlagConfiguration;
}

private static final String[] MEDAL_EMOJIS = {"🥇", "🥈", "🥉"};
Expand All @@ -79,6 +84,44 @@ private String buildTopUsersSection(final List<UserWithScore> users, final boole
return sb.toString();
}

private void sendWinnerDiscordMessage(
final DiscordClub club, final Leaderboard leaderboard, final List<UserWithScore> winners) {
for (int i = 0; i < Math.min(winners.size(), 3); i++) {
final UserWithScore winner = winners.get(i);
String description = """
Dear %s,

Congratulations! You finished #%d on the %s leaderboard for %s users!

Your final score is: %d points.

Keep up the great work and continue solving problems to maintain your position or climb even higher!

View the full leaderboard at %s/leaderboard/%s?%s=true

Best regards,
The Codebloom Team
""".formatted(
winner.getDiscordName(),
i + 1,
leaderboard.getName(),
club.getName(),
winner.getTotalScore(),
serverUrlUtils.getUrl(),
leaderboard.getId(),
club.getTag().name().toLowerCase());
jdaClient.sendDirectMessageEmbed(
winner.getDiscordId(),
EmbeddedDirectMessageOptions.builder()
.title("%s You placed #%d on %s!".formatted(MEDAL_EMOJIS[i], i + 1, leaderboard.getName()))
.description(description)
.footerText("Codebloom - LeetCode Leaderboard for %s".formatted(club.getName()))
.footerIcon("%s/favicon.ico".formatted(serverUrlUtils.getUrl()))
.color(new Color(69, 129, 103))
.build());
}
}

/**
* Loads the page for each discord club's tag, takes a screenshot, and sends a final Discord message with the
* winners of the leaderboard.
Expand Down Expand Up @@ -153,6 +196,9 @@ private void sendLeaderboardCompletedDiscordMessage(final DiscordClub club) {
.footerIcon("%s/favicon.ico".formatted(serverUrlUtils.getUrl()))
.color(new Color(69, 129, 103))
.build());
if (featureFlagConfiguration.isMessageWinners()) {
sendWinnerDiscordMessage(club, currentLeaderboard.get(), users);
}
} catch (Exception e) {
log.error("Exception thrown in DiscordClubManager", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.utils.FileUpload;
import org.patinanetwork.codebloom.jda.JDAClientManager;
import org.patinanetwork.codebloom.jda.client.options.EmbeddedDirectMessageOptions;
import org.patinanetwork.codebloom.jda.client.options.EmbeddedImagesMessageOptions;
import org.patinanetwork.codebloom.jda.client.options.EmbeddedMessageOptions;
import org.patinanetwork.codebloom.jda.properties.patina.JDAPatinaProperties;
Expand Down Expand Up @@ -138,7 +139,7 @@
EmbedBuilder baseEmbed = new EmbedBuilder()
.setColor(options.getColor())
.setTitle(options.getTitle())
.setUrl("https://codebloom.patinanetwork.org")

Check failure on line 142 in src/main/java/org/patinanetwork/codebloom/jda/client/JDAClient.java

View check run for this annotation

SonarQubeCloud / [codebloom_backend] SonarCloud Code Analysis

Define a constant instead of duplicating this literal "https://codebloom.patinanetwork.org" 3 times.

See more on https://sonarcloud.io/project/issues?id=Patina-Network_codebloom_backend&issues=AaHMHY8amfo7WjWP5mej&open=AaHMHY8amfo7WjWP5mej&pullRequest=1079
.setDescription(options.getDescription())
.setFooter(options.getFooterText(), options.getFooterIcon());

Expand Down Expand Up @@ -187,4 +188,21 @@
return false;
}
}

public void sendDirectMessageEmbed(final String userId, final EmbeddedDirectMessageOptions options) {
EmbedBuilder embed = new EmbedBuilder()
.setColor(options.getColor())
.setTitle(options.getTitle())
.setUrl("https://codebloom.patinanetwork.org")
.setDescription(options.getDescription())
.setFooter(options.getFooterText(), options.getFooterIcon());

jda.retrieveUserById(userId)
.flatMap(user -> user.openPrivateChannel())
.flatMap(channel -> channel.sendMessageEmbeds(embed.build()))
.queue(
msg -> log.info("DM sent to user {}", userId),
// User may have DMs disabled
throwable -> log.warn("Failed to DM user {}", userId, throwable));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.patinanetwork.codebloom.jda.client.options;

import java.awt.Color;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class EmbeddedDirectMessageOptions {
private String title;
private String description;
private String footerText;
private String footerIcon;
private Color color;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class FeatureFlagConfiguration {

private boolean userMetrics;

private boolean messageWinners;

/** no need to track the object, backend just writes this directly to the index.html file */
private Map<String, Boolean> ui = new HashMap<>();
}
1 change: 1 addition & 0 deletions src/main/resources/application-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ codebloom:
ff:
duels: true
user-metrics: false
message-winners: true
ui:
duels: true
userMetrics: false
1 change: 1 addition & 0 deletions src/main/resources/application-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ codebloom:
ff:
duels: true
user-metrics: true
message-winners: true
ui:
duels: true
userMetrics: true
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ ff:
# override per environment as needed
duels: false
user-metrics: false
message-winners: false
# Passed straight through to the UI: keys must be camelCase since they are copied straight to the UI as is.
ui:
duels: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import org.patinanetwork.codebloom.common.page.Indexed;
import org.patinanetwork.codebloom.common.url.ServerUrlUtils;
import org.patinanetwork.codebloom.jda.client.JDAClient;
import org.patinanetwork.codebloom.jda.client.options.EmbeddedDirectMessageOptions;
import org.patinanetwork.codebloom.jda.client.options.EmbeddedImagesMessageOptions;
import org.patinanetwork.codebloom.jda.properties.FeatureFlagConfiguration;
import org.slf4j.LoggerFactory;

public class DiscordClubManagerTest {
Expand All @@ -44,7 +46,7 @@ public class DiscordClubManagerTest {
private ServerUrlUtils serverUrlUtils = mock(ServerUrlUtils.class);
private UserRepository userRepository = mock(UserRepository.class);
private LeaderboardManager leaderboardManager = mock(LeaderboardManager.class);

private FeatureFlagConfiguration featureFlagConfiguration = mock(FeatureFlagConfiguration.class);
private DiscordClubManager discordClubManager;

private ListAppender<ILoggingEvent> logWatcher;
Expand All @@ -57,7 +59,8 @@ void setUp() {
leaderboardRepository,
discordClubRepository,
userRepository,
leaderboardManager);
leaderboardManager,
featureFlagConfiguration);

logWatcher = new ListAppender<>();
logWatcher.start();
Expand Down Expand Up @@ -185,6 +188,7 @@ void testSendLeaderboardCompletedDiscordMessageFiltersZeroPointUsers() {
void testSendLeaderboardCompletedDiscordMessageAllUsersHaveZeroPoints() {
DiscordClub mockClub = createMockDiscordClub("Test Club", Tag.Rpi);
when(discordClubRepository.getAllActiveDiscordClubs()).thenReturn(Arrays.asList(mockClub));
when(featureFlagConfiguration.isMessageWinners()).thenReturn(true);

setupMockLeaderboardDataWithAllZeroScoreUsers();

Expand All @@ -193,6 +197,7 @@ void testSendLeaderboardCompletedDiscordMessageAllUsersHaveZeroPoints() {
ArgumentCaptor<EmbeddedImagesMessageOptions> captor =
ArgumentCaptor.forClass(EmbeddedImagesMessageOptions.class);
verify(jdaClient).sendEmbedWithImages(captor.capture());
verify(jdaClient, never()).sendDirectMessageEmbed(anyString(), any(EmbeddedDirectMessageOptions.class));

String description = captor.getValue().getDescription();
assertTrue(description.contains("No one claimed a spot on this leaderboard"));
Expand All @@ -214,6 +219,7 @@ void testSendWeeklyLeaderboardUpdateDiscordMessageFiltersZeroPointUsers() {

verify(discordClubRepository).getAllActiveDiscordClubs();
verify(jdaClient).sendEmbedWithImages(any(EmbeddedImagesMessageOptions.class));
verify(jdaClient, never()).sendDirectMessageEmbed(any(String.class), any(EmbeddedDirectMessageOptions.class));
}

@Test
Expand All @@ -229,6 +235,7 @@ void testSendWeeklyLeaderboardUpdateDiscordMessageAllUsersHaveZeroPoints() {
ArgumentCaptor<EmbeddedImagesMessageOptions> captor =
ArgumentCaptor.forClass(EmbeddedImagesMessageOptions.class);
verify(jdaClient).sendEmbedWithImages(captor.capture());
verify(jdaClient, never()).sendDirectMessageEmbed(anyString(), any(EmbeddedDirectMessageOptions.class));

String description = captor.getValue().getDescription();
assertTrue(description.contains("no scores yet"));
Expand All @@ -241,38 +248,70 @@ void testSendWeeklyLeaderboardUpdateDiscordMessageAllUsersHaveZeroPoints() {
void testSendLeaderboardCompletedDiscordMessageOnlyOneUserWithPoints() {
DiscordClub mockClub = createMockDiscordClub("Test Club", Tag.Rpi);
when(discordClubRepository.getAllActiveDiscordClubs()).thenReturn(Arrays.asList(mockClub));
when(featureFlagConfiguration.isMessageWinners()).thenReturn(true);

setupMockLeaderboardDataWithOneUser();

discordClubManager.sendLeaderboardCompletedDiscordMessageToAllClubs();

ArgumentCaptor<EmbeddedImagesMessageOptions> captor =
ArgumentCaptor<EmbeddedImagesMessageOptions> leaderboardCaptor =
ArgumentCaptor.forClass(EmbeddedImagesMessageOptions.class);
verify(jdaClient).sendEmbedWithImages(captor.capture());
ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<EmbeddedDirectMessageOptions> embedCaptor =
ArgumentCaptor.forClass(EmbeddedDirectMessageOptions.class);
verify(jdaClient).sendEmbedWithImages(leaderboardCaptor.capture());
verify(jdaClient).sendDirectMessageEmbed(idCaptor.capture(), embedCaptor.capture());

String description = captor.getValue().getDescription();
assertTrue(description.contains("🥇"));
assertFalse(description.contains("🥈"));
assertFalse(description.contains("🥉"));
String leaderboardDescription = leaderboardCaptor.getValue().getDescription();
assertTrue(leaderboardDescription.contains("🥇"));
assertFalse(leaderboardDescription.contains("🥈"));
assertFalse(leaderboardDescription.contains("🥉"));

assertEquals("discord1", idCaptor.getValue());
assertTrue(embedCaptor.getValue().getTitle().contains("🥇"));
}

@Test
void testSendLeaderboardCompletedDiscordMessageMessageWinnersFlagOffSkipsDirectMessages() {
DiscordClub mockClub = createMockDiscordClub("Test Club", Tag.Rpi);
when(discordClubRepository.getAllActiveDiscordClubs()).thenReturn(Arrays.asList(mockClub));
when(featureFlagConfiguration.isMessageWinners()).thenReturn(false);

setupMockLeaderboardDataWithOneUser();

discordClubManager.sendLeaderboardCompletedDiscordMessageToAllClubs();

verify(jdaClient).sendEmbedWithImages(any(EmbeddedImagesMessageOptions.class));
verify(jdaClient, never()).sendDirectMessageEmbed(anyString(), any(EmbeddedDirectMessageOptions.class));
}

@Test
void testSendLeaderboardCompletedDiscordMessageOnlyTwoUsersWithPoints() {
DiscordClub mockClub = createMockDiscordClub("Test Club", Tag.Rpi);
when(discordClubRepository.getAllActiveDiscordClubs()).thenReturn(Arrays.asList(mockClub));
when(featureFlagConfiguration.isMessageWinners()).thenReturn(true);

setupMockLeaderboardDataWithTwoUsers();

discordClubManager.sendLeaderboardCompletedDiscordMessageToAllClubs();

ArgumentCaptor<EmbeddedImagesMessageOptions> captor =
ArgumentCaptor<EmbeddedImagesMessageOptions> leaderboardCaptor =
ArgumentCaptor.forClass(EmbeddedImagesMessageOptions.class);
verify(jdaClient).sendEmbedWithImages(captor.capture());
ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<EmbeddedDirectMessageOptions> embedCaptor =
ArgumentCaptor.forClass(EmbeddedDirectMessageOptions.class);

String description = captor.getValue().getDescription();
assertTrue(description.contains("🥇"));
assertTrue(description.contains("🥈"));
assertFalse(description.contains("🥉"));
verify(jdaClient).sendEmbedWithImages(leaderboardCaptor.capture());
verify(jdaClient, times(2)).sendDirectMessageEmbed(idCaptor.capture(), embedCaptor.capture());

String leaderboardDescription = leaderboardCaptor.getValue().getDescription();
assertTrue(leaderboardDescription.contains("🥇"));
assertTrue(leaderboardDescription.contains("🥈"));
assertFalse(leaderboardDescription.contains("🥉"));

assertEquals(List.of("discord1", "discord2"), idCaptor.getAllValues());
assertTrue(embedCaptor.getAllValues().get(0).getTitle().contains("🥇"));
assertTrue(embedCaptor.getAllValues().get(1).getTitle().contains("🥈"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ void setUp() {
FeatureFlagConfiguration ff = new FeatureFlagConfiguration();
ff.setDuels(true);
ff.setUserMetrics(false);
ff.setMessageWinners(true);
featureFlagManager = new FeatureFlagManager(ff);
}

@Test
void getAllFlagsReturnsValuesFromConfiguration() {
var flags = featureFlagManager.getFlags();

assertEquals(2, flags.size());
assertEquals(3, flags.size());
assertEquals(Boolean.TRUE, flags.get("duels"));
assertEquals(Boolean.FALSE, flags.get("userMetrics"));
assertEquals(Boolean.TRUE, flags.get("messageWinners"));
}

@Test
Expand Down
Loading