Skip to content
Merged
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 @@ -2,20 +2,37 @@

import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import net.dv8tion.jda.api.requests.restaction.WebhookMessageCreateAction;
import net.dv8tion.jda.api.utils.FileUpload;
import net.javadiscord.javabot.data.config.BotConfig;
import net.javadiscord.javabot.data.h2db.DbActions;
import net.javadiscord.javabot.systems.help.HelpExperienceService;
import net.javadiscord.javabot.systems.help.dao.HelpTransactionRepository;
import net.javadiscord.javabot.systems.help.model.HelpAccount;
import net.javadiscord.javabot.util.Checks;
import net.javadiscord.javabot.util.ExceptionLogger;
import net.javadiscord.javabot.util.Pair;
import net.javadiscord.javabot.util.Plotter;
import net.javadiscord.javabot.util.Responses;
import net.javadiscord.javabot.util.StringUtils;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;

import org.jetbrains.annotations.NotNull;
import org.springframework.dao.DataAccessException;
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
Expand All @@ -27,26 +44,40 @@
*/
public class HelpAccountSubcommand extends SlashCommand.Subcommand {

private final BotConfig botConfig;
private final DbActions dbActions;
private final HelpExperienceService helpExperienceService;
private final HelpTransactionRepository transactionRepository;

/**
* The constructor of this class, which sets the corresponding {@link SubcommandData}.
*
* @param botConfig The bot configuration
* @param dbActions An object responsible for various database actions
* @param helpExperienceService Service object that handles Help Experience Transactions.
* @param transactionRepository DAO for help XP transactions
*/
public HelpAccountSubcommand(DbActions dbActions, HelpExperienceService helpExperienceService) {
public HelpAccountSubcommand(BotConfig botConfig, DbActions dbActions, HelpExperienceService helpExperienceService, HelpTransactionRepository transactionRepository) {
this.dbActions = dbActions;
this.helpExperienceService = helpExperienceService;
this.botConfig = botConfig;
this.transactionRepository = transactionRepository;
setCommandData(new SubcommandData("account", "Shows an overview of your Help Account.")
.addOption(OptionType.USER, "user", "If set, show the Help Account of the specified user instead.", false)
.addOption(OptionType.BOOLEAN, "plot", "generate a plot of help XP history", false)
);
}

@Override
public void execute(@NotNull SlashCommandInteractionEvent event) {
User user = event.getOption("user", event::getUser, OptionMapping::getAsUser);
boolean plot = event.getOption("plot", false, OptionMapping::getAsBoolean);

if (plot && user.getIdLong()!=event.getUser().getIdLong() && !Checks.hasStaffRole(botConfig, event.getMember())) {
Responses.error(event, "You can only plot your own help XP history.").queue();
return;
}

long totalThanks = dbActions.count(
"SELECT COUNT(id) FROM help_channel_thanks WHERE helper_id = ?",
s -> s.setLong(1, user.getIdLong())
Expand All @@ -55,15 +86,59 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
"SELECT COUNT(id) FROM help_channel_thanks WHERE helper_id = ? AND thanked_at > DATEADD('week', -1, CURRENT_TIMESTAMP(0))",
s -> s.setLong(1, user.getIdLong())
);

event.deferReply().queue();

FileUpload upload = null;
if (plot) {
upload = generatePlot(user);
}

try {
HelpAccount account = helpExperienceService.getOrCreateAccount(user.getIdLong());
event.replyEmbeds(buildHelpAccountEmbed(account, user, event.getGuild(), totalThanks, weekThanks)).queue();
WebhookMessageCreateAction<Message> reply = event.getHook().sendMessageEmbeds(buildHelpAccountEmbed(account, user, event.getGuild(), totalThanks, weekThanks));
if (upload!=null) {
reply.addFiles(upload);
}
reply.queue();
} catch (DataAccessException e) {
ExceptionLogger.capture(e, getClass().getSimpleName());
Responses.error(event, e.getMessage()).queue();
}
}

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

if (xpData.isEmpty()) {
return null;
}

List<Pair<String, Double>> plotData = new ArrayList<>();

int i = 0;
for(LocalDate position = LocalDate.now().minusYears(1); position.isBefore(LocalDate.now().plusDays(1)); position=position.plusMonths(1)) {
double value = 0.0;
if(i<xpData.size()) {
Pair<Pair<Integer, Integer>, Double> entry = xpData.get(i);
if(entry.first().first() == position.getMonthValue() && entry.first().second() == position.getYear()) {
value = Math.round(entry.second()*100)/100.0;
i++;
}
}
plotData.add(new Pair<>(position.getMonth() + " " + position.getYear(), value));
}

BufferedImage plt = new Plotter(plotData).plot();
try(ByteArrayOutputStream os = new ByteArrayOutputStream()){
ImageIO.write(plt, "png", os);
return FileUpload.fromData(os.toByteArray(), "image.png");
} catch (IOException e) {
ExceptionLogger.capture(e, "Cannot create XP plot");
}
return null;
}

private @NotNull MessageEmbed buildHelpAccountEmbed(HelpAccount account, @NotNull User user, Guild guild, long totalThanks, long weekThanks) {
return new EmbedBuilder()
.setAuthor(user.getAsTag(), null, user.getEffectiveAvatarUrl())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.javadiscord.javabot.systems.help.model.HelpTransaction;
import net.javadiscord.javabot.util.Pair;

import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
Expand All @@ -11,6 +13,7 @@

import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -85,6 +88,18 @@ private HelpTransaction read(ResultSet rs) throws SQLException {
return transaction;
}

/**
* Gets the total earned XP of a user since a specific timestamp grouped by months.
* @param userId the user to get XP from
* @param start the start timestamp
* @return a list consisting of month, year and the total XP earned that month
*/
public List<Pair<Pair<Integer, Integer>, Double>> getTotalTransactionWeightByMonth(long userId, LocalDateTime start) {
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",
(rs, row)-> new Pair<>(new Pair<>(rs.getInt("m"), rs.getInt("y")), rs.getDouble("total")),
userId, start);
}

/**
* Checks whether a transaction with a specific recipient exists in a specific channel.
* @param recipient The ID of the recipient
Expand Down
83 changes: 83 additions & 0 deletions src/main/java/net/javadiscord/javabot/util/Plotter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package net.javadiscord.javabot.util;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.List;

/**
* Creates diagrams.
*/
public class Plotter {
private final List<Pair<String, Double>> entries;
private int width=3000;
private int height=1500;

/**
* Creates the plotter.
* @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
*/
public Plotter(List<Pair<String, Double>> entries) {
this.entries=entries;
}

/**
* Create a diagram from the data supplied to the constructor.
* @return the diagram as a {@link BufferedImage}
*/
public BufferedImage plot() {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = img.createGraphics();

g2d.setFont(ImageGenerationUtils.getResourceFont("assets/fonts/Uni-Sans-Heavy.ttf", 30).orElseThrow());

g2d.setBackground(Color.WHITE);
g2d.fillRect(0, 0, width, height);
g2d.setColor(Color.BLACK);

centeredText(g2d, "gained help XP per month", width/2, 50);

plotEntries(g2d, 100, 100, width-200, height-200);

return img;
}

private void plotEntries(Graphics2D g2d, int x, int y, int width, int height) {
double maxValue = entries.stream().mapToDouble(Pair::second).sum();
int stepSize = 2*(int)Math.pow(10,(int)Math.log10(maxValue)-1);
if (stepSize==0) {
stepSize=1;
}
maxValue += stepSize;

int numEntries = entries.size();

int currentX = x;

g2d.drawLine(x, y, x, y+height);

if(maxValue>0) {
for (int current = 0; current < maxValue; current += stepSize) {
g2d.drawString(String.valueOf(current), 95-g2d.getFontMetrics().stringWidth(String.valueOf(current)), this.height-(y+(height*current)/(int)maxValue)+g2d.getFontMetrics().getHeight()/3);
}
}

boolean shift=false;
for (Pair<String, Double> entry : entries) {
int shiftNum = shift ? g2d.getFontMetrics().getHeight() : 0;
centeredText(g2d, entry.first(), currentX+(width/(2*numEntries)), this.height-y/2+shiftNum);
int entryHeight = (int)(height*entry.second()/maxValue);
g2d.setColor(Color.GRAY);
g2d.fillRect(currentX, this.height-y-entryHeight, width/numEntries, entryHeight);
g2d.setColor(Color.BLACK);
g2d.drawRect(currentX, this.height-y-entryHeight, width/numEntries, entryHeight);
centeredText(g2d, String.valueOf(entry.second()), currentX+(width/(2*numEntries)), this.height-y-entryHeight-10);
shift=!shift;
currentX += width/numEntries;
}
}

private void centeredText(Graphics2D g2d, String text, int x, int y) {
g2d.drawString(text, x-g2d.getFontMetrics().stringWidth(text)/2, y);
}
}