Skip to content

Commit c830f7f

Browse files
authored
Merge pull request #416 from danthe1st/help-history-chart
plotting of help XP
2 parents b81420e + db33cb2 commit c830f7f

File tree

3 files changed

+175
-2
lines changed

3 files changed

+175
-2
lines changed

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

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,37 @@
22

33
import net.dv8tion.jda.api.EmbedBuilder;
44
import net.dv8tion.jda.api.entities.Guild;
5+
import net.dv8tion.jda.api.entities.Message;
56
import net.dv8tion.jda.api.entities.MessageEmbed;
67
import net.dv8tion.jda.api.entities.Role;
78
import net.dv8tion.jda.api.entities.User;
89
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
910
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
1011
import net.dv8tion.jda.api.interactions.commands.OptionType;
1112
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
13+
import net.dv8tion.jda.api.requests.restaction.WebhookMessageCreateAction;
14+
import net.dv8tion.jda.api.utils.FileUpload;
15+
import net.javadiscord.javabot.data.config.BotConfig;
1216
import net.javadiscord.javabot.data.h2db.DbActions;
1317
import net.javadiscord.javabot.systems.help.HelpExperienceService;
18+
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository;
1419
import net.javadiscord.javabot.systems.help.model.HelpAccount;
20+
import net.javadiscord.javabot.util.Checks;
1521
import net.javadiscord.javabot.util.ExceptionLogger;
1622
import net.javadiscord.javabot.util.Pair;
23+
import net.javadiscord.javabot.util.Plotter;
1724
import net.javadiscord.javabot.util.Responses;
1825
import net.javadiscord.javabot.util.StringUtils;
26+
27+
import java.awt.image.BufferedImage;
28+
import java.io.ByteArrayOutputStream;
29+
import java.io.IOException;
30+
import java.time.LocalDate;
31+
import java.util.ArrayList;
32+
import java.util.List;
33+
34+
import javax.imageio.ImageIO;
35+
1936
import org.jetbrains.annotations.NotNull;
2037
import org.springframework.dao.DataAccessException;
2138
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
@@ -27,26 +44,40 @@
2744
*/
2845
public class HelpAccountSubcommand extends SlashCommand.Subcommand {
2946

47+
private final BotConfig botConfig;
3048
private final DbActions dbActions;
3149
private final HelpExperienceService helpExperienceService;
50+
private final HelpTransactionRepository transactionRepository;
3251

3352
/**
3453
* The constructor of this class, which sets the corresponding {@link SubcommandData}.
3554
*
55+
* @param botConfig The bot configuration
3656
* @param dbActions An object responsible for various database actions
3757
* @param helpExperienceService Service object that handles Help Experience Transactions.
58+
* @param transactionRepository DAO for help XP transactions
3859
*/
39-
public HelpAccountSubcommand(DbActions dbActions, HelpExperienceService helpExperienceService) {
60+
public HelpAccountSubcommand(BotConfig botConfig, DbActions dbActions, HelpExperienceService helpExperienceService, HelpTransactionRepository transactionRepository) {
4061
this.dbActions = dbActions;
4162
this.helpExperienceService = helpExperienceService;
63+
this.botConfig = botConfig;
64+
this.transactionRepository = transactionRepository;
4265
setCommandData(new SubcommandData("account", "Shows an overview of your Help Account.")
4366
.addOption(OptionType.USER, "user", "If set, show the Help Account of the specified user instead.", false)
67+
.addOption(OptionType.BOOLEAN, "plot", "generate a plot of help XP history", false)
4468
);
4569
}
4670

4771
@Override
4872
public void execute(@NotNull SlashCommandInteractionEvent event) {
4973
User user = event.getOption("user", event::getUser, OptionMapping::getAsUser);
74+
boolean plot = event.getOption("plot", false, OptionMapping::getAsBoolean);
75+
76+
if (plot && user.getIdLong()!=event.getUser().getIdLong() && !Checks.hasStaffRole(botConfig, event.getMember())) {
77+
Responses.error(event, "You can only plot your own help XP history.").queue();
78+
return;
79+
}
80+
5081
long totalThanks = dbActions.count(
5182
"SELECT COUNT(id) FROM help_channel_thanks WHERE helper_id = ?",
5283
s -> s.setLong(1, user.getIdLong())
@@ -55,15 +86,59 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
5586
"SELECT COUNT(id) FROM help_channel_thanks WHERE helper_id = ? AND thanked_at > DATEADD('week', -1, CURRENT_TIMESTAMP(0))",
5687
s -> s.setLong(1, user.getIdLong())
5788
);
89+
90+
event.deferReply().queue();
91+
92+
FileUpload upload = null;
93+
if (plot) {
94+
upload = generatePlot(user);
95+
}
96+
5897
try {
5998
HelpAccount account = helpExperienceService.getOrCreateAccount(user.getIdLong());
60-
event.replyEmbeds(buildHelpAccountEmbed(account, user, event.getGuild(), totalThanks, weekThanks)).queue();
99+
WebhookMessageCreateAction<Message> reply = event.getHook().sendMessageEmbeds(buildHelpAccountEmbed(account, user, event.getGuild(), totalThanks, weekThanks));
100+
if (upload!=null) {
101+
reply.addFiles(upload);
102+
}
103+
reply.queue();
61104
} catch (DataAccessException e) {
62105
ExceptionLogger.capture(e, getClass().getSimpleName());
63106
Responses.error(event, e.getMessage()).queue();
64107
}
65108
}
66109

110+
private FileUpload generatePlot(User user) {
111+
List<Pair<Pair<Integer,Integer>,Double>> xpData = transactionRepository.getTotalTransactionWeightByMonth(user.getIdLong(), LocalDate.now().withDayOfMonth(1).minusYears(1).atStartOfDay());
112+
113+
if (xpData.isEmpty()) {
114+
return null;
115+
}
116+
117+
List<Pair<String, Double>> plotData = new ArrayList<>();
118+
119+
int i = 0;
120+
for(LocalDate position = LocalDate.now().minusYears(1); position.isBefore(LocalDate.now().plusDays(1)); position=position.plusMonths(1)) {
121+
double value = 0.0;
122+
if(i<xpData.size()) {
123+
Pair<Pair<Integer, Integer>, Double> entry = xpData.get(i);
124+
if(entry.first().first() == position.getMonthValue() && entry.first().second() == position.getYear()) {
125+
value = Math.round(entry.second()*100)/100.0;
126+
i++;
127+
}
128+
}
129+
plotData.add(new Pair<>(position.getMonth() + " " + position.getYear(), value));
130+
}
131+
132+
BufferedImage plt = new Plotter(plotData).plot();
133+
try(ByteArrayOutputStream os = new ByteArrayOutputStream()){
134+
ImageIO.write(plt, "png", os);
135+
return FileUpload.fromData(os.toByteArray(), "image.png");
136+
} catch (IOException e) {
137+
ExceptionLogger.capture(e, "Cannot create XP plot");
138+
}
139+
return null;
140+
}
141+
67142
private @NotNull MessageEmbed buildHelpAccountEmbed(HelpAccount account, @NotNull User user, Guild guild, long totalThanks, long weekThanks) {
68143
return new EmbedBuilder()
69144
.setAuthor(user.getAsTag(), null, user.getEffectiveAvatarUrl())

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import lombok.RequiredArgsConstructor;
44
import lombok.extern.slf4j.Slf4j;
55
import net.javadiscord.javabot.systems.help.model.HelpTransaction;
6+
import net.javadiscord.javabot.util.Pair;
7+
68
import org.springframework.dao.DataAccessException;
79
import org.springframework.dao.EmptyResultDataAccessException;
810
import org.springframework.jdbc.core.JdbcTemplate;
@@ -11,6 +13,7 @@
1113

1214
import java.sql.ResultSet;
1315
import java.sql.SQLException;
16+
import java.time.LocalDateTime;
1417
import java.util.List;
1518
import java.util.Map;
1619
import java.util.Optional;
@@ -85,6 +88,18 @@ private HelpTransaction read(ResultSet rs) throws SQLException {
8588
return transaction;
8689
}
8790

91+
/**
92+
* Gets the total earned XP of a user since a specific timestamp grouped by months.
93+
* @param userId the user to get XP from
94+
* @param start the start timestamp
95+
* @return a list consisting of month, year and the total XP earned that month
96+
*/
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")),
100+
userId, start);
101+
}
102+
88103
/**
89104
* Checks whether a transaction with a specific recipient exists in a specific channel.
90105
* @param recipient The ID of the recipient
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package net.javadiscord.javabot.util;
2+
3+
import java.awt.Color;
4+
import java.awt.Graphics2D;
5+
import java.awt.image.BufferedImage;
6+
import java.util.List;
7+
8+
/**
9+
* Creates diagrams.
10+
*/
11+
public class Plotter {
12+
private final List<Pair<String, Double>> entries;
13+
private int width=3000;
14+
private int height=1500;
15+
16+
/**
17+
* Creates the plotter.
18+
* @param entries a list of all data points to plot, each represented as a {@link Pair} consisting of the name and value of the data point
19+
*/
20+
public Plotter(List<Pair<String, Double>> entries) {
21+
this.entries=entries;
22+
}
23+
24+
/**
25+
* Create a diagram from the data supplied to the constructor.
26+
* @return the diagram as a {@link BufferedImage}
27+
*/
28+
public BufferedImage plot() {
29+
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
30+
Graphics2D g2d = img.createGraphics();
31+
32+
g2d.setFont(ImageGenerationUtils.getResourceFont("assets/fonts/Uni-Sans-Heavy.ttf", 30).orElseThrow());
33+
34+
g2d.setBackground(Color.WHITE);
35+
g2d.fillRect(0, 0, width, height);
36+
g2d.setColor(Color.BLACK);
37+
38+
centeredText(g2d, "gained help XP per month", width/2, 50);
39+
40+
plotEntries(g2d, 100, 100, width-200, height-200);
41+
42+
return img;
43+
}
44+
45+
private void plotEntries(Graphics2D g2d, int x, int y, int width, int height) {
46+
double maxValue = entries.stream().mapToDouble(Pair::second).sum();
47+
int stepSize = 2*(int)Math.pow(10,(int)Math.log10(maxValue)-1);
48+
if (stepSize==0) {
49+
stepSize=1;
50+
}
51+
maxValue += stepSize;
52+
53+
int numEntries = entries.size();
54+
55+
int currentX = x;
56+
57+
g2d.drawLine(x, y, x, y+height);
58+
59+
if(maxValue>0) {
60+
for (int current = 0; current < maxValue; current += stepSize) {
61+
g2d.drawString(String.valueOf(current), 95-g2d.getFontMetrics().stringWidth(String.valueOf(current)), this.height-(y+(height*current)/(int)maxValue)+g2d.getFontMetrics().getHeight()/3);
62+
}
63+
}
64+
65+
boolean shift=false;
66+
for (Pair<String, Double> entry : entries) {
67+
int shiftNum = shift ? g2d.getFontMetrics().getHeight() : 0;
68+
centeredText(g2d, entry.first(), currentX+(width/(2*numEntries)), this.height-y/2+shiftNum);
69+
int entryHeight = (int)(height*entry.second()/maxValue);
70+
g2d.setColor(Color.GRAY);
71+
g2d.fillRect(currentX, this.height-y-entryHeight, width/numEntries, entryHeight);
72+
g2d.setColor(Color.BLACK);
73+
g2d.drawRect(currentX, this.height-y-entryHeight, width/numEntries, entryHeight);
74+
centeredText(g2d, String.valueOf(entry.second()), currentX+(width/(2*numEntries)), this.height-y-entryHeight-10);
75+
shift=!shift;
76+
currentX += width/numEntries;
77+
}
78+
}
79+
80+
private void centeredText(Graphics2D g2d, String text, int x, int y) {
81+
g2d.drawString(text, x-g2d.getFontMetrics().stringWidth(text)/2, y);
82+
}
83+
}

0 commit comments

Comments
 (0)