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 @@ -15,13 +15,15 @@
*/
public class DelegatingCommandHandler implements SlashCommandHandler {
private final Map<String, SlashCommandHandler> subcommandHandlers;
private final Map<String, SlashCommandHandler> subcommandGroupHandlers;

/**
* Constructs the handler with an already-initialized map of subcommands.
* @param subcommandHandlers The map of subcommands to use.
*/
public DelegatingCommandHandler(Map<String, SlashCommandHandler> subcommandHandlers) {
this.subcommandHandlers = subcommandHandlers;
this.subcommandGroupHandlers = new HashMap<>();
}

/**
Expand All @@ -30,6 +32,7 @@ public DelegatingCommandHandler(Map<String, SlashCommandHandler> subcommandHandl
*/
public DelegatingCommandHandler() {
this.subcommandHandlers = new HashMap<>();
this.subcommandGroupHandlers = new HashMap<>();
}

/**
Expand All @@ -41,6 +44,15 @@ public Map<String, SlashCommandHandler> getSubcommandHandlers() {
return Collections.unmodifiableMap(this.subcommandHandlers);
}

/**
* Gets an unmodifiable map of the subcommand group handlers that this
* handler has registered.
* @return An unmodifiable map containing all registered group handlers.
*/
public Map<String, SlashCommandHandler> getSubcommandGroupHandlers() {
return Collections.unmodifiableMap(this.subcommandGroupHandlers);
}

/**
* Adds a subcommand to this handler.
* @param name The name of the subcommand. <em>This is case-sensitive.</em>
Expand All @@ -53,6 +65,18 @@ protected void addSubcommand(String name, SlashCommandHandler handler) {
this.subcommandHandlers.put(name, handler);
}

/**
* Adds a subcommand group handler to this handler.
* @param name The name of the subcommand group. <em>This is case-sensitive.</em>
* @param handler The handler that will be called to handle commands within
* the given subcommand's name.
* @throws UnsupportedOperationException If this handler was initialized
* with an unmodifiable map of subcommand group handlers.
*/
protected void addSubcommandGroup(String name, SlashCommandHandler handler) {
this.subcommandGroupHandlers.put(name, handler);
}

/**
* Handles slash command events by checking if a subcommand name was given,
* and if so, delegating the handling of the event to that subcommand.
Expand All @@ -61,6 +85,14 @@ protected void addSubcommand(String name, SlashCommandHandler handler) {
*/
@Override
public ReplyAction handle(SlashCommandEvent event) {
// First we check if the event has specified a subcommand group, and if we have a group handler for it.
if (event.getSubcommandGroup() != null) {
SlashCommandHandler groupHandler = this.getSubcommandGroupHandlers().get(event.getSubcommandGroup());
if (groupHandler != null) {
return groupHandler.handle(event);
}
}
// If the event doesn't have a subcommand group, or no handler was found for the group, we just move on to the subcommand.
if (event.getSubcommandName() == null) {
return this.handleNonSubcommand(event);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.javadiscord.javabot.qotw;

import com.javadiscord.javabot.commands.DelegatingCommandHandler;
import com.javadiscord.javabot.qotw.subcommands.AddQuestionSubcommand;
import com.javadiscord.javabot.qotw.subcommands.ListQuestionsSubcommand;
import com.javadiscord.javabot.qotw.subcommands.RemoveQuestionSubcommand;

import java.util.Map;

public class QOTWCommandHandler extends DelegatingCommandHandler {
public QOTWCommandHandler() {
this.addSubcommandGroup("questions-queue", new DelegatingCommandHandler(Map.of(
"list", new ListQuestionsSubcommand(),
"add", new AddQuestionSubcommand(),
"remove", new RemoveQuestionSubcommand()
)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.javadiscord.javabot.qotw.dao;

import com.javadiscord.javabot.qotw.model.QOTWQuestion;
import lombok.RequiredArgsConstructor;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
public class QuestionRepository {
private final Connection con;

public void save(QOTWQuestion question) throws SQLException {
PreparedStatement stmt = con.prepareStatement(
"INSERT INTO qotw_question (guild_id, created_by, text, priority) VALUES (?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
);
stmt.setLong(1, question.getGuildId());
stmt.setLong(2, question.getCreatedBy());
stmt.setString(3, question.getText());
stmt.setInt(4, question.getPriority());
int rows = stmt.executeUpdate();
if (rows == 0) throw new SQLException("New question was not inserted.");
ResultSet rs = stmt.getGeneratedKeys();
if (rs.next()) {
question.setId(rs.getLong(1));
}
stmt.close();
}

public List<QOTWQuestion> getQuestions(long guildId, int page, int size) throws SQLException {
String sql = "SELECT * FROM qotw_question WHERE guild_id = ? AND used = FALSE ORDER BY priority DESC, created_at ASC LIMIT %d OFFSET %d";
PreparedStatement stmt = con.prepareStatement(String.format(sql, size, page));
stmt.setLong(1, guildId);
ResultSet rs = stmt.executeQuery();
List<QOTWQuestion> questions = new ArrayList<>(size);
while (rs.next()) {
questions.add(this.read(rs));
}
stmt.close();
return questions;
}

public Optional<QOTWQuestion> getNextQuestion(long guildId) throws SQLException {
PreparedStatement stmt = con.prepareStatement("SELECT * FROM qotw_question WHERE guild_id = ? AND used = FALSE ORDER BY priority DESC, created_at LIMIT 1");
stmt.setLong(1, guildId);
ResultSet rs = stmt.executeQuery();
Optional<QOTWQuestion> optionalQuestion;
if (rs.next()) {
optionalQuestion = Optional.of(this.read(rs));
} else {
optionalQuestion = Optional.empty();
}
stmt.close();
return optionalQuestion;
}

public boolean removeQuestion(long guildId, long id) throws SQLException {
PreparedStatement stmt = con.prepareStatement("DELETE FROM qotw_question WHERE guild_id = ? AND id = ?");
stmt.setLong(1, guildId);
stmt.setLong(2, id);
int rows = stmt.executeUpdate();
stmt.close();
return rows > 0;
}

private QOTWQuestion read(ResultSet rs) throws SQLException {
QOTWQuestion question = new QOTWQuestion();
question.setId(rs.getLong("id"));
question.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
question.setGuildId(rs.getLong("guild_id"));
question.setCreatedBy(rs.getLong("created_by"));
question.setText(rs.getString("text"));
question.setUsed(rs.getBoolean("used"));
question.setPriority(rs.getInt("priority"));
return question;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.javadiscord.javabot.qotw.model;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.Objects;

/**
* Represents a single QOTW question.
*/
@Data
@NoArgsConstructor
public class QOTWQuestion implements Comparable<QOTWQuestion> {
private long id;
private LocalDateTime createdAt;
private long guildId;
private long createdBy;
private String text;
private boolean used;
private int priority;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof QOTWQuestion)) return false;
QOTWQuestion that = (QOTWQuestion) o;
if (this.getId() == that.getId()) return true;
return this.getText().equals(that.getText()) && this.getGuildId() == that.getGuildId() && this.getCreatedBy() == that.getCreatedBy() && this.getPriority() == that.getPriority() && this.isUsed() == that.isUsed();
}

@Override
public int hashCode() {
return Objects.hash(getId(), getCreatedAt(), getGuildId(), getCreatedBy(), getText(), isUsed(), getPriority());
}

@Override
public int compareTo(QOTWQuestion o) {
int result = Integer.compare(this.getPriority(), o.getPriority());
if (result == 0) {
return this.getCreatedAt().compareTo(o.getCreatedAt());
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* This package contains all components pertaining to the Question of the Week
* system, which involves storing a queue of questions in the database, from
* which a new question is pulled from each week.
*/
package com.javadiscord.javabot.qotw;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.javadiscord.javabot.qotw.subcommands;

import com.javadiscord.javabot.commands.Responses;
import com.javadiscord.javabot.qotw.dao.QuestionRepository;
import com.javadiscord.javabot.qotw.model.QOTWQuestion;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction;

import java.sql.Connection;

public class AddQuestionSubcommand extends QOTWSubcommand {
@Override
protected ReplyAction handleCommand(SlashCommandEvent event, Connection con, long guildId) throws Exception {
QOTWQuestion question = new QOTWQuestion();
question.setGuildId(guildId);
question.setCreatedBy(event.getUser().getIdLong());
question.setPriority(0);

OptionMapping textOption = event.getOption("question");
if (textOption == null) {
return Responses.warning(event, "Missing required arguments.");
}

String text = textOption.getAsString();
if (text.isBlank() || text.length() > 1024) {
return Responses.warning(event, "Invalid question text. Must not be blank, and must be less than 1024 characters.");
}
question.setText(text);

OptionMapping priorityOption = event.getOption("priority");
if (priorityOption != null) {
question.setPriority((int) priorityOption.getAsLong());
}

new QuestionRepository(con).save(question);
return Responses.success(event, "Question Added", "Your question has been added to the queue. Its id is `" + question.getId() + "`.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.javadiscord.javabot.qotw.subcommands;

import com.javadiscord.javabot.Bot;
import com.javadiscord.javabot.commands.Responses;
import com.javadiscord.javabot.qotw.dao.QuestionRepository;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction;

import java.sql.Connection;
import java.time.format.DateTimeFormatter;

public class ListQuestionsSubcommand extends QOTWSubcommand {
@Override
protected ReplyAction handleCommand(SlashCommandEvent event, Connection con, long guildId) throws Exception {
var repository = new QuestionRepository(con);
OptionMapping pageOption = event.getOption("page");
int page = 0;
if (pageOption != null) {
int userPage = (int) pageOption.getAsLong();
if (userPage < 0) {
return Responses.warning(event, "Invalid page.");
}
page = userPage;
}

var questions = repository.getQuestions(guildId, page, 10);
EmbedBuilder embedBuilder = new EmbedBuilder()
.setTitle("QOTW Questions Queue");
if (questions.isEmpty()) {
embedBuilder.setDescription("There are no questions in the queue.");
return event.replyEmbeds(embedBuilder.build());
}
Bot.asyncPool.submit(() -> {
for (var question : questions) {
embedBuilder.addField(
String.valueOf(question.getId()),
String.format(
"> %s\nPriority: **%d**\nCreated by: %s\nCreated at: %s",
question.getText(),
question.getPriority(),
event.getJDA().retrieveUserById(question.getCreatedBy()).complete().getAsTag(),
question.getCreatedAt().format(DateTimeFormatter.ofPattern("d MMMM yyyy"))
),
false
);
}
event.getHook().sendMessageEmbeds(embedBuilder.build()).queue();
});
return event.deferReply();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.javadiscord.javabot.qotw.subcommands;

import com.javadiscord.javabot.Bot;
import com.javadiscord.javabot.commands.Responses;
import com.javadiscord.javabot.commands.SlashCommandHandler;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction;

import java.sql.Connection;

/**
* Abstract parent class for all QOTW subcommands, which handles the standard
* behavior of preparing a connection and obtaining the guild id; these two
* things are required for all QOTW subcommands.
*/
public abstract class QOTWSubcommand implements SlashCommandHandler {
@Override
public ReplyAction handle(SlashCommandEvent event) {
if (event.getGuild() == null) {
return Responses.warning(event, "This command can only be used in the context of a guild.");
}

try (Connection con = Bot.dataSource.getConnection()) {
con.setAutoCommit(false);
var reply = this.handleCommand(event, con, event.getGuild().getIdLong());
con.commit();
return reply;
} catch (Exception e) {
e.printStackTrace();
return Responses.error(event, "An error occurred: " + e.getMessage());
}
}

protected abstract ReplyAction handleCommand(SlashCommandEvent event, Connection con, long guildId) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.javadiscord.javabot.qotw.subcommands;

import com.javadiscord.javabot.commands.Responses;
import com.javadiscord.javabot.qotw.dao.QuestionRepository;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction;

import java.sql.Connection;

public class RemoveQuestionSubcommand extends QOTWSubcommand {
@Override
protected ReplyAction handleCommand(SlashCommandEvent event, Connection con, long guildId) throws Exception {
OptionMapping idOption = event.getOption("id");
if (idOption == null) {
return Responses.warning(event, "Missing required arguments.");
}

long id = idOption.getAsLong();
boolean removed = new QuestionRepository(con).removeQuestion(guildId, id);
if (removed) {
return Responses.success(event, "Question Removed", "The question with id `" + id + "` has been removed.");
} else {
return Responses.warning(event, "Could not remove question with id `" + id + "`. Are you sure it exists?");
}
}
}
Loading