Skip to content

Commit 6ffe450

Browse files
Merge pull request #443 from danthe1st/monthly-help-leaderboard
2 parents 4c7ea70 + 1c4aaf5 commit 6ffe450

File tree

6 files changed

+322
-125
lines changed

6 files changed

+322
-125
lines changed

src/main/java/net/javadiscord/javabot/systems/help/HelpExperienceService.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository;
1212
import net.javadiscord.javabot.systems.help.model.HelpAccount;
1313
import net.javadiscord.javabot.systems.help.model.HelpTransaction;
14+
import net.javadiscord.javabot.systems.user_commands.leaderboard.ExperienceLeaderboardSubcommand;
1415
import net.javadiscord.javabot.util.ExceptionLogger;
16+
import net.javadiscord.javabot.util.ImageCache;
1517
import net.javadiscord.javabot.util.Pair;
1618
import org.jetbrains.annotations.NotNull;
1719
import org.springframework.dao.DataAccessException;
@@ -97,6 +99,7 @@ public void performTransaction(long recipient, double value, Guild guild, long c
9799
helpTransactionRepository.save(transaction);
98100
checkExperienceRoles(guild, account);
99101
log.info("Added {} help experience to {}'s help account", value, recipient);
102+
ImageCache.removeCachedImagesByKeyword(ExperienceLeaderboardSubcommand.CACHE_PREFIX);
100103
}
101104

102105
private void checkExperienceRoles(@NotNull Guild guild, @NotNull HelpAccount account) {
@@ -114,10 +117,10 @@ private void checkExperienceRoles(@NotNull Guild guild, @NotNull HelpAccount acc
114117
}
115118
}), e -> {});
116119
}
117-
120+
118121
/**
119122
* add XP to all helpers depending on the messages they sent.
120-
*
123+
*
121124
* @param post The {@link ThreadChannel} post
122125
* @param allowIfXPAlreadyGiven {@code true} if XP should be awarded if XP have already been awarded
123126
*/

src/main/java/net/javadiscord/javabot/systems/help/dao/HelpTransactionRepository.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,36 @@ private HelpTransaction read(ResultSet rs) throws SQLException {
9595
* @return a list consisting of month, year and the total XP earned that month
9696
*/
9797
public List<Pair<Pair<Integer, Integer>, Double>> getTotalTransactionWeightByMonth(long userId, LocalDateTime start) {
98-
return jdbcTemplate.query("SELECT SUM(weight) AS total, EXTRACT(MONTH FROM created_at) AS m, EXTRACT(YEAR FROM created_at) AS y FROM help_transaction WHERE recipient = ? AND created_at >= ? GROUP BY m, y ORDER BY y ASC, m ASC",
99-
(rs, row)-> new Pair<>(new Pair<>(rs.getInt("m"), rs.getInt("y")), rs.getDouble("total")),
98+
return jdbcTemplate.query("SELECT SUM(weight) AS total, EXTRACT(MONTH FROM created_at) AS m, EXTRACT(YEAR FROM created_at) AS y FROM help_transaction WHERE recipient = ? AND created_at >= ? GROUP BY m, y ORDER BY y ASC, m ASC",
99+
(rs, row)-> new Pair<>(new Pair<>(rs.getInt("m"), rs.getInt("y")), rs.getDouble("total")),
100100
userId, start);
101101
}
102102

103+
/**
104+
* Gets the number of users that earned help XP in the last 30 days.
105+
* This corresponds to the number of elements in {@link HelpTransactionRepository#getTotalTransactionWeightsInLastMonth(int, int)}
106+
* @return number of users earning help XP in the last 30 days
107+
*/
108+
public int getNumberOfUsersWithHelpXPInLastMonth() {
109+
return jdbcTemplate.queryForObject("SELECT COUNT(DISTINCT recipient) FROM help_transaction WHERE created_at >= ?",
110+
(rs, row) -> rs.getInt(1),
111+
LocalDateTime.now().minusDays(30));
112+
}
113+
114+
/**
115+
* Gets the total XP of users in the last 30 days in descending order of XP.
116+
* This query uses pagination.
117+
* @param page the page to request
118+
* @param pageSize the number of users
119+
* @return the requested user IDs as well as their XP counts
120+
* @see HelpTransactionRepository#getNumberOfUsersWithHelpXPInLastMonth()
121+
*/
122+
public List<Pair<Long, Integer>> getTotalTransactionWeightsInLastMonth(int page, int pageSize) {
123+
return jdbcTemplate.query("SELECT recipient, SUM(weight) experience FROM help_transaction WHERE created_at >= ? GROUP BY recipient ORDER BY experience DESC LIMIT ? OFFSET ?",
124+
(rs, row) -> new Pair<>(rs.getLong(1), rs.getInt(2)),
125+
LocalDateTime.now().minusDays(30), pageSize, page);
126+
}
127+
103128
/**
104129
* Checks whether a transaction with a specific recipient exists in a specific channel.
105130
* @param recipient The ID of the recipient

src/main/java/net/javadiscord/javabot/systems/user_commands/leaderboard/ExperienceLeaderboardSubcommand.java

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,55 +6,78 @@
66
import xyz.dynxsty.dih4jda.interactions.components.ButtonHandler;
77
import net.dv8tion.jda.api.EmbedBuilder;
88
import net.dv8tion.jda.api.entities.Guild;
9+
import net.dv8tion.jda.api.entities.Member;
910
import net.dv8tion.jda.api.entities.MessageEmbed;
1011
import net.dv8tion.jda.api.entities.Role;
11-
import net.dv8tion.jda.api.entities.User;
1212
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
1313
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
1414
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
1515
import net.dv8tion.jda.api.interactions.commands.OptionType;
16+
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
1617
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
1718
import net.dv8tion.jda.api.interactions.components.ActionRow;
1819
import net.dv8tion.jda.api.interactions.components.buttons.Button;
20+
import net.dv8tion.jda.api.utils.FileUpload;
21+
import net.dv8tion.jda.api.utils.messages.MessageEditBuilder;
1922
import net.javadiscord.javabot.annotations.AutoDetectableComponentHandler;
2023
import net.javadiscord.javabot.systems.help.dao.HelpAccountRepository;
21-
import net.javadiscord.javabot.systems.help.model.HelpAccount;
24+
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository;
2225
import net.javadiscord.javabot.util.ExceptionLogger;
2326
import net.javadiscord.javabot.util.Pair;
2427
import net.javadiscord.javabot.util.Responses;
2528
import org.jetbrains.annotations.Contract;
2629
import org.jetbrains.annotations.NotNull;
2730
import org.springframework.dao.DataAccessException;
2831

32+
import java.io.IOException;
2933
import java.util.List;
34+
import java.util.Objects;
3035
import java.util.concurrent.ExecutorService;
36+
import java.util.function.BiFunction;
3137

3238
/**
3339
* <h3>This class represents the /leaderboard help-experience command.</h3>
3440
*/
3541
@AutoDetectableComponentHandler("experience-leaderboard")
3642
public class ExperienceLeaderboardSubcommand extends SlashCommand.Subcommand implements ButtonHandler {
37-
private static final int PAGE_SIZE = 5;
43+
/**
44+
* prefix contained in the image cache.
45+
*/
46+
public static final String CACHE_PREFIX = "xp_leaderboard";
47+
private static final int PAGE_SIZE = 10;
48+
3849
private final ExecutorService asyncPool;
3950
private final HelpAccountRepository helpAccountRepository;
51+
private final HelpTransactionRepository helpTransactionRepository;
4052

4153
/**
4254
* The constructor of this class, which sets the corresponding {@link SubcommandData}.
4355
* @param helpAccountRepository Dao object that represents the HELP_ACCOUNT SQL Table.
4456
* @param asyncPool the main thread pool for asynchronous operations
57+
* @param helpTransactionRepository Dao object that represents the HELP_TRANSACTIONS SQL Table.
4558
*/
46-
public ExperienceLeaderboardSubcommand(HelpAccountRepository helpAccountRepository, ExecutorService asyncPool) {
59+
public ExperienceLeaderboardSubcommand(HelpAccountRepository helpAccountRepository, ExecutorService asyncPool, HelpTransactionRepository helpTransactionRepository) {
4760
this.asyncPool = asyncPool;
4861
this.helpAccountRepository = helpAccountRepository;
62+
this.helpTransactionRepository = helpTransactionRepository;
4963
setCommandData(new SubcommandData("help-experience", "The Help Experience Leaderboard.")
5064
.addOption(OptionType.INTEGER, "page", "The page of results to show. By default it starts at 1.", false)
65+
.addOptions(new OptionData(OptionType.STRING, "type", "Type of the help-XP headerboard", false)
66+
.addChoice("total", LeaderboardType.TOTAL.name())
67+
.addChoice("last 30 days", LeaderboardType.MONTH.name()))
5168
);
5269
}
5370

5471
@Override
5572
public void handleButton(@NotNull ButtonInteractionEvent event, Button button) {
5673
event.deferEdit().queue();
5774
String[] id = ComponentIdBuilder.split(event.getComponentId());
75+
LeaderboardType type;
76+
if (id.length > 3) {
77+
type = LeaderboardType.valueOf(id[3]);
78+
} else {
79+
type = LeaderboardType.TOTAL;
80+
}
5881
asyncPool.execute(() -> {
5982
try {
6083
int page = Integer.parseInt(id[2]);
@@ -64,60 +87,120 @@ public void handleButton(@NotNull ButtonInteractionEvent event, Button button) {
6487
} else {
6588
page++;
6689
}
67-
int maxPage = helpAccountRepository.getTotalAccounts() / PAGE_SIZE;
90+
int totalAccounts = switch (type) {
91+
case MONTH -> helpTransactionRepository.getNumberOfUsersWithHelpXPInLastMonth();
92+
case TOTAL -> helpAccountRepository.getTotalAccounts();
93+
};
94+
int maxPage = totalAccounts / PAGE_SIZE;
6895
if (page <= 0) {
6996
page = maxPage;
7097
}
7198
if (page > maxPage) {
7299
page = 1;
73100
}
101+
Pair<MessageEmbed, FileUpload> messageInfo = buildExperienceLeaderboard(event.getGuild(), page, type);
74102
event.getHook()
75-
.editOriginalEmbeds(buildExperienceLeaderboard(event.getGuild(), helpAccountRepository, page))
76-
.setComponents(buildPageControls(page)).queue();
77-
} catch (DataAccessException e) {
103+
.editOriginal(new MessageEditBuilder().setEmbeds(messageInfo.first()).setAttachments(messageInfo.second()).build())
104+
.setComponents(buildPageControls(page, type)).queue();
105+
} catch (DataAccessException | IOException e) {
78106
ExceptionLogger.capture(e, ExperienceLeaderboardSubcommand.class.getSimpleName());
79107
}
80108
});
81109
}
82110

83-
private static @NotNull MessageEmbed buildExperienceLeaderboard(Guild guild, @NotNull HelpAccountRepository dao, int page) throws DataAccessException {
84-
int maxPage = dao.getTotalAccounts() / PAGE_SIZE;
85-
List<HelpAccount> accounts = dao.getAccounts(Math.min(page, maxPage), PAGE_SIZE);
111+
112+
private @NotNull Pair<MessageEmbed, FileUpload> buildExperienceLeaderboard(Guild guild, int page, LeaderboardType type) throws DataAccessException, IOException {
113+
return switch (type) {
114+
case TOTAL -> buildGenericExperienceLeaderboard(page, helpAccountRepository.getTotalAccounts(),
115+
"total Leaderboard of help experience",
116+
helpAccountRepository::getAccounts, (position, account) -> {
117+
Pair<Role, Double> currentRole = account.getCurrentExperienceGoal(guild);
118+
return createUserData(guild, position, account.getExperience(), account.getUserId(), currentRole.first() != null ? currentRole.first().getAsMention() + ": " : "");
119+
});
120+
case MONTH -> buildGenericExperienceLeaderboard(page, helpTransactionRepository.getNumberOfUsersWithHelpXPInLastMonth(),
121+
"""
122+
help experience leaderboard from the last 30 days
123+
This leaderboard does not include experience decay.
124+
""",
125+
helpTransactionRepository::getTotalTransactionWeightsInLastMonth, (position, xpInfo) -> {
126+
return createUserData(guild, position, (double) xpInfo.second(), xpInfo.first(), "");
127+
});
128+
};
129+
}
130+
131+
private <T> @NotNull Pair<MessageEmbed, FileUpload> buildGenericExperienceLeaderboard(int page, int totalAccounts, String description,
132+
BiFunction<Integer, Integer, List<T>> accountsReader, BiFunction<Integer, T, UserData> fieldExtractor) throws DataAccessException, IOException {
133+
134+
int maxPage = totalAccounts / PAGE_SIZE;
135+
int actualPage = Math.max(1, Math.min(page, maxPage));
136+
List<T> accounts = accountsReader.apply(actualPage, PAGE_SIZE);
137+
86138
EmbedBuilder builder = new EmbedBuilder()
87139
.setTitle("Experience Leaderboard")
140+
.setDescription(description)
88141
.setColor(Responses.Type.DEFAULT.getColor())
89-
.setFooter(String.format("Page %s/%s", Math.min(page, maxPage), maxPage));
90-
accounts.forEach(account -> {
91-
Pair<Role, Double> currentRole = account.getCurrentExperienceGoal(guild);
92-
User user = guild.getJDA().getUserById(account.getUserId());
93-
builder.addField(
94-
String.format("**%s.** %s", (accounts.indexOf(account) + 1) + (page - 1) * PAGE_SIZE, user == null ? account.getUserId() : UserUtils.getUserTag(user)),
95-
String.format("%s`%.0f XP`\n", currentRole.first() != null ? currentRole.first().getAsMention() + ": " : "", account.getExperience()),
96-
false);
142+
.setFooter(String.format("Page %s/%s", actualPage, maxPage));
143+
144+
String pageCachePrefix = CACHE_PREFIX + "_" + page;
145+
String cacheName = pageCachePrefix + "_" + accounts.hashCode();
146+
byte[] bytes = LeaderboardCreator.attemptLoadFromCache(cacheName, ()->{
147+
try (LeaderboardCreator creator = new LeaderboardCreator(accounts.size(), null)){
148+
for (int i = 0; i < accounts.size(); i++) {
149+
int position = (i + 1) + (actualPage - 1) * PAGE_SIZE;
150+
UserData userInfo = fieldExtractor.apply(position, accounts.get(i));
151+
creator.drawLeaderboardEntry(userInfo.member(), userInfo.displayName(), userInfo.xp(), position);
152+
}
153+
return creator.getImageBytes(cacheName, pageCachePrefix);
154+
}
97155
});
98-
return builder.build();
156+
builder.setImage("attachment://leaderboard.png");
157+
return new Pair<MessageEmbed, FileUpload>(builder.build(), FileUpload.fromData(bytes, "leaderboard.png"));
158+
}
159+
160+
private UserData createUserData(Guild guild, Integer position, double experience, long userId, String prefix) {
161+
Member member = guild.getMemberById(userId);
162+
String displayName;
163+
if (member == null) {
164+
displayName = String.valueOf(userId);
165+
} else {
166+
displayName = UserUtils.getUserTag(member.getUser());
167+
}
168+
return new UserData(member, displayName, (long)experience);
99169
}
100170

101171
@Contract("_ -> new")
102-
private static @NotNull ActionRow buildPageControls(int currentPage) {
172+
private static @NotNull ActionRow buildPageControls(int currentPage, LeaderboardType type) {
103173
return ActionRow.of(
104-
Button.primary(ComponentIdBuilder.build("experience-leaderboard", "left", currentPage), "Prev"),
105-
Button.primary(ComponentIdBuilder.build("experience-leaderboard", "right", currentPage), "Next")
174+
Button.primary(ComponentIdBuilder.build("experience-leaderboard", "left", currentPage, type.name()), "Prev"),
175+
Button.primary(ComponentIdBuilder.build("experience-leaderboard", "right", currentPage, type.name()), "Next")
106176
);
107177
}
108178

109179
@Override
110180
public void execute(@NotNull SlashCommandInteractionEvent event) {
111181
int page = event.getOption("page", 1, OptionMapping::getAsInt);
182+
LeaderboardType type = event.getOption("type", LeaderboardType.TOTAL, o->LeaderboardType.valueOf(o.getAsString()));
112183
event.deferReply().queue();
113184
asyncPool.execute(() -> {
114185
try {
115-
event.getHook().sendMessageEmbeds(buildExperienceLeaderboard(event.getGuild(), helpAccountRepository, page))
116-
.setComponents(buildPageControls(page))
186+
Pair<MessageEmbed, FileUpload> messageInfo = buildExperienceLeaderboard(event.getGuild(), page, type);
187+
event.getHook().sendMessageEmbeds(messageInfo.first())
188+
.addFiles(messageInfo.second())
189+
.setComponents(buildPageControls(page, type))
117190
.queue();
118-
}catch (DataAccessException e) {
191+
}catch (DataAccessException | IOException e) {
119192
ExceptionLogger.capture(e, ExperienceLeaderboardSubcommand.class.getSimpleName());
120193
}
121194
});
122195
}
196+
197+
private enum LeaderboardType{
198+
TOTAL, MONTH
199+
}
200+
201+
private record UserData(Member member, String displayName, long xp) {
202+
UserData {
203+
Objects.requireNonNull(displayName);
204+
}
205+
}
123206
}

src/main/java/net/javadiscord/javabot/systems/user_commands/leaderboard/LeaderboardCommand.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import net.javadiscord.javabot.data.h2db.DbActions;
88
import net.javadiscord.javabot.data.h2db.DbHelper;
99
import net.javadiscord.javabot.systems.help.dao.HelpAccountRepository;
10+
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository;
1011
import net.javadiscord.javabot.systems.qotw.QOTWPointsService;
1112
import net.javadiscord.javabot.systems.qotw.dao.QuestionPointsRepository;
1213

@@ -21,15 +22,16 @@ public class LeaderboardCommand extends SlashCommand {
2122
* @param dbHelper An object managing databse operations
2223
* @param dbActions A utility object providing various operations on the main database
2324
* @param helpAccountRepository Dao object that represents the HELP_ACCOUNT SQL Table.
25+
* @param helpTransactionRepository Dao object that represents the HELP_TRANSACTIONS SQL Table.
2426
* @param qotwPointsRepository Dao object that represents the QOTW_POINTS SQL Table.
2527
*/
26-
public LeaderboardCommand(QOTWPointsService pointsService, ExecutorService asyncPool, DbHelper dbHelper, DbActions dbActions, HelpAccountRepository helpAccountRepository, QuestionPointsRepository qotwPointsRepository) {
28+
public LeaderboardCommand(QOTWPointsService pointsService, ExecutorService asyncPool, DbHelper dbHelper, DbActions dbActions, HelpAccountRepository helpAccountRepository, HelpTransactionRepository helpTransactionRepository, QuestionPointsRepository qotwPointsRepository) {
2729
setCommandData(Commands.slash("leaderboard", "Command for all leaderboards.")
2830
.setGuildOnly(true)
2931
);
3032
addSubcommands(
3133
new QOTWLeaderboardSubcommand(pointsService, asyncPool, qotwPointsRepository),
3234
new ThanksLeaderboardSubcommand(asyncPool, dbActions),
33-
new ExperienceLeaderboardSubcommand(helpAccountRepository, asyncPool));
35+
new ExperienceLeaderboardSubcommand(helpAccountRepository, asyncPool, helpTransactionRepository));
3436
}
3537
}

0 commit comments

Comments
 (0)