Skip to content

Commit ff22fd0

Browse files
committed
30 days help XP leaderboard
1 parent b5c9859 commit ff22fd0

File tree

3 files changed

+104
-26
lines changed

3 files changed

+104
-26
lines changed

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: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@
77
import net.dv8tion.jda.api.EmbedBuilder;
88
import net.dv8tion.jda.api.entities.Guild;
99
import net.dv8tion.jda.api.entities.MessageEmbed;
10+
import net.dv8tion.jda.api.entities.MessageEmbed.Field;
1011
import net.dv8tion.jda.api.entities.Role;
1112
import net.dv8tion.jda.api.entities.User;
1213
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
1314
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
1415
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
1516
import net.dv8tion.jda.api.interactions.commands.OptionType;
17+
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
1618
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
1719
import net.dv8tion.jda.api.interactions.components.ActionRow;
1820
import net.dv8tion.jda.api.interactions.components.buttons.Button;
1921
import net.javadiscord.javabot.annotations.AutoDetectableComponentHandler;
2022
import net.javadiscord.javabot.systems.help.dao.HelpAccountRepository;
21-
import net.javadiscord.javabot.systems.help.model.HelpAccount;
23+
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository;
2224
import net.javadiscord.javabot.util.ExceptionLogger;
2325
import net.javadiscord.javabot.util.Pair;
2426
import net.javadiscord.javabot.util.Responses;
@@ -28,6 +30,7 @@
2830

2931
import java.util.List;
3032
import java.util.concurrent.ExecutorService;
33+
import java.util.function.BiFunction;
3134

3235
/**
3336
* <h3>This class represents the /leaderboard help-experience command.</h3>
@@ -37,24 +40,36 @@ public class ExperienceLeaderboardSubcommand extends SlashCommand.Subcommand imp
3740
private static final int PAGE_SIZE = 5;
3841
private final ExecutorService asyncPool;
3942
private final HelpAccountRepository helpAccountRepository;
43+
private final HelpTransactionRepository helpTransactionRepository;
4044

4145
/**
4246
* The constructor of this class, which sets the corresponding {@link SubcommandData}.
4347
* @param helpAccountRepository Dao object that represents the HELP_ACCOUNT SQL Table.
4448
* @param asyncPool the main thread pool for asynchronous operations
49+
* @param helpTransactionRepository Dao object that represents the HELP_TRANSACTIONS SQL Table.
4550
*/
46-
public ExperienceLeaderboardSubcommand(HelpAccountRepository helpAccountRepository, ExecutorService asyncPool) {
51+
public ExperienceLeaderboardSubcommand(HelpAccountRepository helpAccountRepository, ExecutorService asyncPool, HelpTransactionRepository helpTransactionRepository) {
4752
this.asyncPool = asyncPool;
4853
this.helpAccountRepository = helpAccountRepository;
54+
this.helpTransactionRepository = helpTransactionRepository;
4955
setCommandData(new SubcommandData("help-experience", "The Help Experience Leaderboard.")
5056
.addOption(OptionType.INTEGER, "page", "The page of results to show. By default it starts at 1.", false)
57+
.addOptions(new OptionData(OptionType.STRING, "type", "Type of the help-XP headerboard", false)
58+
.addChoice("total", LeaderboardType.TOTAL.name())
59+
.addChoice("last 30 days", LeaderboardType.MONTH.name()))
5160
);
5261
}
5362

5463
@Override
5564
public void handleButton(@NotNull ButtonInteractionEvent event, Button button) {
5665
event.deferEdit().queue();
5766
String[] id = ComponentIdBuilder.split(event.getComponentId());
67+
LeaderboardType type;
68+
if (id.length > 3) {
69+
type = LeaderboardType.valueOf(id[3]);
70+
} else {
71+
type = LeaderboardType.TOTAL;
72+
}
5873
asyncPool.execute(() -> {
5974
try {
6075
int page = Integer.parseInt(id[2]);
@@ -64,60 +79,96 @@ public void handleButton(@NotNull ButtonInteractionEvent event, Button button) {
6479
} else {
6580
page++;
6681
}
67-
int maxPage = helpAccountRepository.getTotalAccounts() / PAGE_SIZE;
82+
int totalAccounts = switch (type) {
83+
case MONTH -> helpTransactionRepository.getNumberOfUsersWithHelpXPInLastMonth();
84+
case TOTAL -> helpAccountRepository.getTotalAccounts();
85+
};
86+
int maxPage = totalAccounts / PAGE_SIZE;
6887
if (page <= 0) {
6988
page = maxPage;
7089
}
7190
if (page > maxPage) {
7291
page = 1;
7392
}
7493
event.getHook()
75-
.editOriginalEmbeds(buildExperienceLeaderboard(event.getGuild(), helpAccountRepository, page))
76-
.setComponents(buildPageControls(page)).queue();
94+
.editOriginalEmbeds(buildExperienceLeaderboard(event.getGuild(), page, type))
95+
.setComponents(buildPageControls(page, type)).queue();
7796
} catch (DataAccessException e) {
7897
ExceptionLogger.capture(e, ExperienceLeaderboardSubcommand.class.getSimpleName());
7998
}
8099
});
81100
}
82101

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);
102+
103+
private @NotNull MessageEmbed buildExperienceLeaderboard(Guild guild, int page, LeaderboardType type) throws DataAccessException {
104+
return switch (type) {
105+
case TOTAL -> buildGenericExperienceLeaderboard(page, helpAccountRepository.getTotalAccounts(),
106+
"total Leaderboard of help experience",
107+
helpAccountRepository::getAccounts, (position, account) -> {
108+
Pair<Role, Double> currentRole = account.getCurrentExperienceGoal(guild);
109+
return buildEmbed(guild, position, account.getExperience(), account.getUserId(), currentRole.first() != null ? currentRole.first().getAsMention() + ": " : "");
110+
});
111+
case MONTH -> buildGenericExperienceLeaderboard(page, helpTransactionRepository.getNumberOfUsersWithHelpXPInLastMonth(),
112+
"""
113+
help experience leaderboard from the last 30 days
114+
This leaderboard does not include experience decay.
115+
""",
116+
helpTransactionRepository::getTotalTransactionWeightsInLastMonth, (position, xpInfo) -> {
117+
return buildEmbed(guild, position, (double) xpInfo.second(), xpInfo.first(), "");
118+
});
119+
};
120+
}
121+
122+
private <T> @NotNull MessageEmbed buildGenericExperienceLeaderboard(int page, int totalAccounts, String description,
123+
BiFunction<Integer, Integer, List<T>> accountsReader, BiFunction<Integer, T, MessageEmbed.Field> fieldExtractor) throws DataAccessException {
124+
int maxPage = totalAccounts / PAGE_SIZE;
125+
int actualPage = Math.max(1, Math.min(page, maxPage));
126+
List<T> accounts = accountsReader.apply(actualPage, PAGE_SIZE);
86127
EmbedBuilder builder = new EmbedBuilder()
87128
.setTitle("Experience Leaderboard")
129+
.setDescription(description)
88130
.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);
97-
});
131+
.setFooter(String.format("Page %s/%s", actualPage, maxPage));
132+
for (int i = 0; i < accounts.size(); i++) {
133+
int position = (i + 1) + (actualPage - 1) * PAGE_SIZE;
134+
builder.addField(fieldExtractor.apply(position, accounts.get(i)));
135+
}
98136
return builder.build();
99137
}
100138

139+
private Field buildEmbed(Guild guild, Integer position, double experience, long userId, String prefix) {
140+
User user = guild.getJDA().getUserById(userId);
141+
return new MessageEmbed.Field(
142+
String.format("**%s.** %s", position, user == null ? userId : UserUtils.getUserTag(user)),
143+
String.format("%s`%.0f XP`\n", prefix, experience),
144+
false);
145+
}
146+
101147
@Contract("_ -> new")
102-
private static @NotNull ActionRow buildPageControls(int currentPage) {
148+
private static @NotNull ActionRow buildPageControls(int currentPage, LeaderboardType type) {
103149
return ActionRow.of(
104-
Button.primary(ComponentIdBuilder.build("experience-leaderboard", "left", currentPage), "Prev"),
105-
Button.primary(ComponentIdBuilder.build("experience-leaderboard", "right", currentPage), "Next")
150+
Button.primary(ComponentIdBuilder.build("experience-leaderboard", "left", currentPage, type.name()), "Prev"),
151+
Button.primary(ComponentIdBuilder.build("experience-leaderboard", "right", currentPage, type.name()), "Next")
106152
);
107153
}
108154

109155
@Override
110156
public void execute(@NotNull SlashCommandInteractionEvent event) {
111157
int page = event.getOption("page", 1, OptionMapping::getAsInt);
158+
LeaderboardType type = event.getOption("type", LeaderboardType.TOTAL, o->LeaderboardType.valueOf(o.getAsString()));
112159
event.deferReply().queue();
113160
asyncPool.execute(() -> {
114161
try {
115-
event.getHook().sendMessageEmbeds(buildExperienceLeaderboard(event.getGuild(), helpAccountRepository, page))
116-
.setComponents(buildPageControls(page))
162+
event.getHook().sendMessageEmbeds(buildExperienceLeaderboard(event.getGuild(), page, type))
163+
.setComponents(buildPageControls(page, type))
117164
.queue();
118165
}catch (DataAccessException e) {
119166
ExceptionLogger.capture(e, ExperienceLeaderboardSubcommand.class.getSimpleName());
120167
}
121168
});
122169
}
170+
171+
private enum LeaderboardType{
172+
TOTAL, MONTH
173+
}
123174
}

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)