Skip to content

Commit e63ef35

Browse files
authored
Merge pull request #450 from danthe1st/help-stats-command
awarded help XP distribution statistics
2 parents 31000dd + 34c06ad commit e63ef35

File tree

12 files changed

+227
-69
lines changed

12 files changed

+227
-69
lines changed

src/main/java/net/javadiscord/javabot/api/routes/user_profile/UserProfileController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public ResponseEntity<UserProfileData> getUserProfile(
109109
data.setQotwAccount(qotwAccount);
110110
// Help Account
111111
HelpAccount helpAccount = helpExperienceService.getOrCreateAccount(user.getIdLong());
112-
data.setHelpAccount(HelpAccountData.of(helpAccount, guild));
112+
data.setHelpAccount(HelpAccountData.of(botConfig, helpAccount, guild));
113113
// User Preferences
114114
List<UserPreference> preferences = Arrays.stream(Preference.values()).map(p -> preferenceService.getOrCreate(user.getIdLong(), p)).toList();
115115
data.setPreferences(preferences);

src/main/java/net/javadiscord/javabot/api/routes/user_profile/model/HelpAccountData.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import lombok.Data;
44
import net.dv8tion.jda.api.entities.Guild;
55
import net.dv8tion.jda.api.entities.Role;
6+
import net.javadiscord.javabot.data.config.BotConfig;
67
import net.javadiscord.javabot.systems.help.model.HelpAccount;
78
import net.javadiscord.javabot.util.ColorUtils;
89
import net.javadiscord.javabot.util.Pair;
@@ -27,20 +28,21 @@ public class HelpAccountData {
2728
* A simple utility method which creates an instance of this class based on
2829
* the specified {@link HelpAccount}.
2930
*
31+
* @param botConfig configuration of the bot.
3032
* @param account The {@link HelpAccount} to convert.
3133
* @param guild The {@link Guild}.
3234
* @return An instance of the {@link HelpAccountData} class.
3335
*/
34-
public static @NotNull HelpAccountData of(@NotNull HelpAccount account, Guild guild) {
36+
public static @NotNull HelpAccountData of(BotConfig botConfig, @NotNull HelpAccount account, Guild guild) {
3537
HelpAccountData data = new HelpAccountData();
3638
data.setExperienceCurrent(account.getExperience());
37-
Pair<Role, Double> previousRank = account.getPreviousExperienceGoal(guild);
39+
Pair<Role, Double> previousRank = account.getPreviousExperienceGoal(botConfig, guild);
3840
if (previousRank != null && previousRank.first() != null) {
3941
data.setCurrentRank(previousRank.first().getName());
4042
data.setCurrentRankColor(ColorUtils.toString(previousRank.first().getColor()));
4143
data.setExperiencePrevious(previousRank.second());
4244
}
43-
Pair<Role, Double> nextRank = account.getNextExperienceGoal(guild);
45+
Pair<Role, Double> nextRank = account.getNextExperienceGoal(botConfig, guild);
4446
if (nextRank != null && nextRank.first() != null) {
4547
data.setNextRank(nextRank.first().getName());
4648
data.setNextRankColor(ColorUtils.toString(nextRank.first().getColor()));

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public HelpAccount getOrCreateAccount(long userId) throws DataAccessException {
5050
if (optional.isPresent()) {
5151
account = optional.get();
5252
} else {
53-
account = new HelpAccount(botConfig);
53+
account = new HelpAccount();
5454
account.setUserId(userId);
5555
account.setExperience(0);
5656
helpAccountRepository.insert(account);
@@ -105,7 +105,7 @@ public void performTransaction(long recipient, double value, Guild guild, long c
105105
private void checkExperienceRoles(@NotNull Guild guild, @NotNull HelpAccount account) {
106106
guild.retrieveMemberById(account.getUserId()).queue(member ->
107107
botConfig.get(guild).getHelpConfig().getExperienceRoles().forEach((key, value) -> {
108-
Pair<Role, Double> role = account.getCurrentExperienceGoal(guild);
108+
Pair<Role, Double> role = account.getCurrentExperienceGoal(botConfig, guild);
109109
if (role.first() == null) return;
110110
if (key.equals(role.first().getIdLong())) {
111111
guild.addRoleToMember(member, role.first()).queue();

src/main/java/net/javadiscord/javabot/systems/help/commands/HelpAccountSubcommand.java

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
import net.javadiscord.javabot.data.h2db.DbActions;
1717
import net.javadiscord.javabot.systems.help.HelpExperienceService;
1818
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository;
19+
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository.MonthInYear;
1920
import net.javadiscord.javabot.systems.help.model.HelpAccount;
20-
import net.javadiscord.javabot.util.Checks;
2121
import net.javadiscord.javabot.util.ExceptionLogger;
2222
import net.javadiscord.javabot.util.Pair;
2323
import net.javadiscord.javabot.util.Plotter;
@@ -74,11 +74,6 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
7474
User user = event.getOption("user", event::getUser, OptionMapping::getAsUser);
7575
boolean plot = event.getOption("plot", false, OptionMapping::getAsBoolean);
7676

77-
if (plot && user.getIdLong()!=event.getUser().getIdLong() && !Checks.hasStaffRole(botConfig, event.getMember())) {
78-
Responses.error(event, "You can only plot your own help XP history.").queue();
79-
return;
80-
}
81-
8277
long totalThanks = dbActions.count(
8378
"SELECT COUNT(id) FROM help_channel_thanks WHERE helper_id = ?",
8479
s -> s.setLong(1, user.getIdLong())
@@ -109,25 +104,25 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
109104
}
110105

111106
private FileUpload generatePlot(User user) {
112-
List<Pair<Pair<Integer,Integer>,Double>> xpData = transactionRepository.getTotalTransactionWeightByMonth(user.getIdLong(), LocalDate.now().withDayOfMonth(1).minusYears(1).atStartOfDay());
107+
List<Pair<MonthInYear,Double>> xpData = transactionRepository.getTotalTransactionWeightByMonth(user.getIdLong(), LocalDate.now().withDayOfMonth(1).minusYears(1).atStartOfDay());
113108

114109
if (xpData.isEmpty()) {
115110
return null;
116111
}
117112

118-
List<Pair<String, Double>> plotData = new ArrayList<>();
113+
List<Pair<String, Plotter.Bar>> plotData = new ArrayList<>();
119114

120115
int i = 0;
121116
for(LocalDate position = LocalDate.now().minusYears(1); position.isBefore(LocalDate.now().plusDays(1)); position=position.plusMonths(1)) {
122117
double value = 0.0;
123118
if(i<xpData.size()) {
124-
Pair<Pair<Integer, Integer>, Double> entry = xpData.get(i);
125-
if(entry.first().first() == position.getMonthValue() && entry.first().second() == position.getYear()) {
126-
value = Math.round(entry.second()*100)/100.0;
119+
Pair<MonthInYear, Double> entry = xpData.get(i);
120+
if(entry.first().month() == position.getMonthValue() && entry.first().year() == position.getYear()) {
121+
value = entry.second();
127122
i++;
128123
}
129124
}
130-
plotData.add(new Pair<>(position.getMonth() + " " + position.getYear(), value));
125+
plotData.add(new Pair<>(position.getMonth() + " " + position.getYear(), new Plotter.Bar(value)));
131126
}
132127

133128
BufferedImage plt = new Plotter(plotData, "gained help XP per month").plot();
@@ -156,9 +151,9 @@ private FileUpload generatePlot(User user) {
156151
}
157152

158153
private @NotNull String formatExperience(Guild guild, @NotNull HelpAccount account) {
159-
Pair<Role, Double> previousRoleAndXp = account.getPreviousExperienceGoal(guild);
160-
Pair<Role, Double> currentRoleAndXp = account.getCurrentExperienceGoal(guild);
161-
Pair<Role, Double> nextRoleAndXp = account.getNextExperienceGoal(guild);
154+
Pair<Role, Double> previousRoleAndXp = account.getPreviousExperienceGoal(botConfig, guild);
155+
Pair<Role, Double> currentRoleAndXp = account.getCurrentExperienceGoal(botConfig, guild);
156+
Pair<Role, Double> nextRoleAndXp = account.getNextExperienceGoal(botConfig, guild);
162157
double currentXp = account.getExperience() - (previousRoleAndXp == null ? 0 : previousRoleAndXp.second());
163158
double goalXp = nextRoleAndXp.second() - (previousRoleAndXp == null ? 0 : previousRoleAndXp.second());
164159
StringBuilder sb = new StringBuilder();

src/main/java/net/javadiscord/javabot/systems/help/commands/HelpCommand.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ public class HelpCommand extends SlashCommand {
1212
* @param helpAccountSubcommand /help account
1313
* @param helpPingSubcommand /help ping
1414
* @param helpGuidelinesSubcommand /help guidelines
15+
* @param helpStatisticsSubcommand /help stats
1516
*/
16-
public HelpCommand(HelpAccountSubcommand helpAccountSubcommand, HelpPingSubcommand helpPingSubcommand, HelpGuidelinesSubcommand helpGuidelinesSubcommand) {
17+
public HelpCommand(HelpAccountSubcommand helpAccountSubcommand, HelpPingSubcommand helpPingSubcommand, HelpGuidelinesSubcommand helpGuidelinesSubcommand, HelpStatisticsSubcommand helpStatisticsSubcommand) {
1718
setCommandData(Commands.slash("help", "Commands related to the help system.")
1819
.setGuildOnly(true)
1920
);
20-
addSubcommands(helpAccountSubcommand, helpPingSubcommand, helpGuidelinesSubcommand);
21+
addSubcommands(helpAccountSubcommand, helpPingSubcommand, helpGuidelinesSubcommand, helpStatisticsSubcommand);
2122
}
2223
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package net.javadiscord.javabot.systems.help.commands;
2+
3+
import java.awt.Color;
4+
import java.awt.image.BufferedImage;
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.IOException;
7+
import java.time.LocalDate;
8+
import java.util.ArrayList;
9+
import java.util.Comparator;
10+
import java.util.HashMap;
11+
import java.util.LinkedHashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Map.Entry;
15+
import java.util.PriorityQueue;
16+
17+
import javax.imageio.ImageIO;
18+
19+
import net.dv8tion.jda.api.EmbedBuilder;
20+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
21+
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
22+
import net.dv8tion.jda.api.utils.FileUpload;
23+
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository;
24+
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository.MonthInYear;
25+
import net.javadiscord.javabot.systems.help.model.HelpAccount;
26+
import net.javadiscord.javabot.util.ExceptionLogger;
27+
import net.javadiscord.javabot.util.Pair;
28+
import net.javadiscord.javabot.util.Plotter;
29+
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
30+
31+
/**
32+
* Shows the distribution of help XP per user.
33+
*/
34+
public class HelpStatisticsSubcommand extends SlashCommand.Subcommand {
35+
36+
private static final List<Pair<String, Color>> COLORS = List.of(
37+
new Pair<>("Red", Color.RED), new Pair<>("Blue", Color.BLUE), new Pair<>("Yellow", Color.YELLOW),
38+
new Pair<>("Green", Color.GREEN), new Pair<>("Cyan", Color.CYAN), new Pair<>("Magenta", Color.MAGENTA),
39+
new Pair<>("Orange", Color.ORANGE), new Pair<>("Pink", Color.PINK), new Pair<>("Light gray", Color.LIGHT_GRAY)
40+
);
41+
42+
private final HelpTransactionRepository transactionRepository;
43+
44+
public HelpStatisticsSubcommand(HelpTransactionRepository transactionRepository) {
45+
this.transactionRepository = transactionRepository;
46+
setCommandData(new SubcommandData("stats", "Shows an general plot about help activity in this server"));
47+
}
48+
49+
@Override
50+
public void execute(SlashCommandInteractionEvent event) {
51+
52+
event.deferReply().queue();
53+
54+
List<Pair<MonthInYear,HelpAccount>> transactionWeights = transactionRepository.getTotalTransactionWeightByMonthAndUsers(LocalDate.now().withDayOfMonth(1).minusYears(1).atStartOfDay());
55+
56+
Map<Long, Pair<String, Color>> topUsersToColors = mapTopUsersToColors(transactionWeights);
57+
58+
List<Pair<String, Plotter.Bar>> plotData = new ArrayList<>();
59+
60+
int i = 0;
61+
62+
for(LocalDate position = LocalDate.now().minusYears(1); position.isBefore(LocalDate.now().plusDays(1)); position=position.plusMonths(1)) {
63+
List<Pair<Color,Double>> entriesForThisMonth = new ArrayList<>();
64+
boolean correctMonth = true;
65+
while(i<transactionWeights.size() && correctMonth) {
66+
Pair<MonthInYear,HelpAccount> entry = transactionWeights.get(i);
67+
if(entry.first().month() == position.getMonthValue() && entry.first().year() == position.getYear()) {
68+
Long userId = entry.second().getUserId();
69+
Color color = topUsersToColors.getOrDefault(userId, new Pair<String, Color>(null, Color.GRAY)).second();
70+
entriesForThisMonth.add(new Pair<>(color, entry.second().getExperience()));
71+
i++;
72+
}else {
73+
correctMonth = false;
74+
}
75+
}
76+
plotData.add(new Pair<>(position.getMonth() + " " + position.getYear(), new Plotter.Bar(entriesForThisMonth)));
77+
}
78+
79+
BufferedImage plot = new Plotter(plotData, "General helper statistics").plot();
80+
try(ByteArrayOutputStream os = new ByteArrayOutputStream()){
81+
ImageIO.write(plot, "png", os);
82+
FileUpload upload = FileUpload.fromData(os.toByteArray(), "image.png");
83+
EmbedBuilder eb = new EmbedBuilder()
84+
.setTitle("Help XP distribution")
85+
.setDescription("This plot shows how much help XP have been awarded to different helpers.")
86+
.setImage("attachment://"+upload.getName());
87+
for (Entry<Long, Pair<String, Color>> entry : topUsersToColors.entrySet()) {
88+
eb.addField(entry.getValue().first(), "<@"+entry.getKey()+">", true);
89+
}
90+
event.getHook().sendMessageEmbeds(eb
91+
.build()).addFiles(upload).queue();
92+
} catch (IOException e) {
93+
ExceptionLogger.capture(e, "Cannot create XP plot");
94+
event.getHook().sendMessage("An error occured.").queue();
95+
}
96+
}
97+
98+
private Map<Long, Pair<String, Color>> mapTopUsersToColors(List<Pair<MonthInYear,HelpAccount>> transactionWeights) {
99+
Map<Long, Double> totalByUser = new HashMap<>();
100+
for (Pair<MonthInYear,HelpAccount> pair : transactionWeights) {
101+
totalByUser.merge(pair.second().getUserId(), pair.second().getExperience(), (a,b) -> a+b);
102+
}
103+
104+
PriorityQueue<Pair<Long, Double>> topUserQueue = new PriorityQueue<>(Comparator.comparingDouble(Pair::second));
105+
for (Entry<Long, Double> e : totalByUser.entrySet()) {
106+
topUserQueue.add(new Pair<>(e.getKey(), e.getValue()));
107+
if(topUserQueue.size()>COLORS.size()) {
108+
topUserQueue.remove();
109+
}
110+
}
111+
112+
List<Long> topUsers = new ArrayList<>();
113+
while(!topUserQueue.isEmpty()) {
114+
topUsers.add(topUserQueue.remove().first());
115+
}
116+
Map<Long, Pair<String, Color>> topUsersToColors = new LinkedHashMap<>();
117+
for(int i=0;i<topUsers.size();i++) {
118+
topUsersToColors.put(topUsers.get(topUsers.size() - i - 1), COLORS.get(i));
119+
}
120+
return topUsersToColors;
121+
}
122+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public Long doInCallableStatement(CallableStatement cs) throws SQLException, Dat
118118
}
119119

120120
private @NotNull HelpAccount read(@NotNull ResultSet rs) throws SQLException {
121-
HelpAccount account = new HelpAccount(botConfig);
121+
HelpAccount account = new HelpAccount();
122122
account.setUserId(rs.getLong("user_id"));
123123
account.setExperience(rs.getDouble("experience"));
124124
return account;

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import lombok.RequiredArgsConstructor;
44
import lombok.extern.slf4j.Slf4j;
5+
import net.javadiscord.javabot.systems.help.model.HelpAccount;
56
import net.javadiscord.javabot.systems.help.model.HelpTransaction;
67
import net.javadiscord.javabot.util.Pair;
78

@@ -94,12 +95,23 @@ private HelpTransaction read(ResultSet rs) throws SQLException {
9495
* @param start the start timestamp
9596
* @return a list consisting of month, year and the total XP earned that month
9697
*/
97-
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+
public List<Pair<MonthInYear, Double>> getTotalTransactionWeightByMonth(long userId, LocalDateTime start) {
99+
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",
100+
(rs, row)-> new Pair<>(new MonthInYear(rs.getInt("m"), rs.getInt("y")), rs.getDouble("total")),
100101
userId, start);
101102
}
102-
103+
104+
/**
105+
* Gets the total earned XP of a user since a specific timestamp grouped by months and users.
106+
* @param start the start timestamp
107+
* @return a list consisting of month, year, user and the total XP earned that month
108+
*/
109+
public List<Pair<MonthInYear, HelpAccount>> getTotalTransactionWeightByMonthAndUsers(LocalDateTime start) {
110+
return jdbcTemplate.query("SELECT SUM(weight) AS total, EXTRACT(MONTH FROM created_at) AS m, EXTRACT(YEAR FROM created_at) AS y, recipient FROM help_transaction WHERE created_at >= ? GROUP BY m, y, recipient ORDER BY y ASC, m ASC, total DESC",
111+
(rs, row)-> new Pair<>(new MonthInYear(rs.getInt("m"), rs.getInt("y")), new HelpAccount(rs.getLong("recipient"), rs.getDouble("total"))),
112+
start);
113+
}
114+
103115
/**
104116
* Gets the number of users that earned help XP in the last 30 days.
105117
* This corresponds to the number of elements in {@link HelpTransactionRepository#getTotalTransactionWeightsInLastMonth(int, int)}
@@ -137,4 +149,11 @@ public boolean existsTransactionWithRecipientInChannel(long recipient, long chan
137149
Integer.class,
138150
recipient, channelId) > 0;
139151
}
152+
153+
/**
154+
* Stores a given month in a specific year.
155+
* @param month the month in the year.
156+
* @param year the year.
157+
*/
158+
public record MonthInYear(int month, int year) {}
140159
}

src/main/java/net/javadiscord/javabot/systems/help/model/HelpAccount.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package net.javadiscord.javabot.systems.help.model;
22

3+
import lombok.AllArgsConstructor;
34
import lombok.Data;
45
import lombok.RequiredArgsConstructor;
56
import net.dv8tion.jda.api.entities.Guild;
@@ -9,8 +10,6 @@
910
import org.jetbrains.annotations.NotNull;
1011
import org.jetbrains.annotations.Nullable;
1112

12-
import com.fasterxml.jackson.annotation.JsonIgnore;
13-
1413
import java.util.Comparator;
1514
import java.util.Map;
1615
import java.util.Optional;
@@ -20,9 +19,8 @@
2019
*/
2120
@Data
2221
@RequiredArgsConstructor
22+
@AllArgsConstructor
2323
public class HelpAccount {
24-
@JsonIgnore
25-
private final BotConfig botConfig;
2624
private long userId;
2725
private double experience;
2826

@@ -33,10 +31,11 @@ public void updateExperience(double change) {
3331
/**
3432
* Tries to get the current experience role.
3533
*
34+
* @param botConfig main configuration.
3635
* @param guild The current {@link Guild}.
3736
* @return A {@link Pair} with both the Role, and the experience needed.
3837
*/
39-
public @NotNull Pair<Role, Double> getCurrentExperienceGoal(Guild guild) {
38+
public @NotNull Pair<Role, Double> getCurrentExperienceGoal(BotConfig botConfig, Guild guild) {
4039
Map<Long, Double> experienceRoles = botConfig.get(guild).getHelpConfig().getExperienceRoles();
4140
Map.Entry<Long, Double> highestExperience = Map.entry(0L, 0.0);
4241
for (Map.Entry<Long, Double> entry : experienceRoles.entrySet()) {
@@ -50,10 +49,11 @@ public void updateExperience(double change) {
5049
/**
5150
* Tries to get the last experience goal.
5251
*
52+
* @param botConfig main configuration.
5353
* @param guild The current {@link Guild}.
5454
* @return The {@link Pair} with both the Role, and the experience needed.
5555
*/
56-
public @Nullable Pair<Role, Double> getPreviousExperienceGoal(Guild guild) {
56+
public @Nullable Pair<Role, Double> getPreviousExperienceGoal(BotConfig botConfig, Guild guild) {
5757
Map<Long, Double> experienceRoles = botConfig.get(guild).getHelpConfig().getExperienceRoles();
5858
Optional<Pair<Role, Double>> experienceOptional = experienceRoles.entrySet().stream()
5959
.filter(r -> r.getValue() < experience)
@@ -65,10 +65,11 @@ public void updateExperience(double change) {
6565
/**
6666
* Tries to get the next experience goal based on the current experience count.
6767
*
68+
* @param botConfig main configuration.
6869
* @param guild The current {@link Guild}.
6970
* @return A {@link Pair} with both the Role, and the experience needed.
7071
*/
71-
public @NotNull Pair<Role, Double> getNextExperienceGoal(Guild guild) {
72+
public @NotNull Pair<Role, Double> getNextExperienceGoal(BotConfig botConfig, Guild guild) {
7273
Map<Long, Double> experienceRoles = botConfig.get(guild).getHelpConfig().getExperienceRoles();
7374
Map.Entry<Long, Double> entry = experienceRoles.entrySet()
7475
.stream()

0 commit comments

Comments
 (0)