diff --git a/build.gradle b/build.gradle index 32e0ed2..3cebd3f 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,8 @@ dependencies { implementation 'org.yaml:snakeyaml:1.29' implementation 'com.google.code.gson:gson:2.8.9' + implementation 'org.openpnp:opencv:4.5.1-2' + // Persistence Dependencies implementation 'org.mongodb:mongodb-driver:3.12.10' implementation 'com.h2database:h2:1.4.200' diff --git a/src/main/java/net/javadiscord/javabot2/Bot.java b/src/main/java/net/javadiscord/javabot2/Bot.java index a4a4ae3..a589c4c 100644 --- a/src/main/java/net/javadiscord/javabot2/Bot.java +++ b/src/main/java/net/javadiscord/javabot2/Bot.java @@ -62,7 +62,7 @@ public static void main(String[] args) { SlashCommandListener commandListener = new SlashCommandListener( api, args.length > 0 && args[0].equalsIgnoreCase("--register-commands"), - "commands/moderation.yaml" + "commands/moderation.yaml", "commands/activity.yaml" ); api.addSlashCommandCreateListener(commandListener); try { diff --git a/src/main/java/net/javadiscord/javabot2/command/DelegatingCommandHandler.java b/src/main/java/net/javadiscord/javabot2/command/DelegatingCommandHandler.java new file mode 100644 index 0000000..e292124 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/DelegatingCommandHandler.java @@ -0,0 +1,109 @@ +package net.javadiscord.javabot2.command; + +import org.javacord.api.interaction.SlashCommandInteraction; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Abstract command handler which is useful for commands which consist of lots + * of subcommands. A child class will supply a map of subcommand handlers, so + * that this parent handler can do the logic of finding the right subcommand to + * invoke depending on the event received. + */ +public class DelegatingCommandHandler implements SlashCommandHandler { + private final Map subcommandHandlers; + private final Map subcommandGroupHandlers; + + /** + * Constructs the handler with an already-initialized map of subcommands. + * @param subcommandHandlers The map of subcommands to use. + */ + public DelegatingCommandHandler(Map subcommandHandlers) { + this.subcommandHandlers = subcommandHandlers; + this.subcommandGroupHandlers = new HashMap<>(); + } + + /** + * Constructs the handler with an empty map, which subcommands can be added + * to via {@link DelegatingCommandHandler#addSubcommand(String, SlashCommandHandler)}. + */ + public DelegatingCommandHandler() { + this.subcommandHandlers = new HashMap<>(); + this.subcommandGroupHandlers = new HashMap<>(); + } + + /** + * Gets an unmodifiable map of the subcommand handlers this delegating + * handler has registered. + * @return An unmodifiable map containing all registered subcommands. + */ + public Map 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 getSubcommandGroupHandlers() { + return Collections.unmodifiableMap(this.subcommandGroupHandlers); + } + + /** + * Adds a subcommand to this handler. + * @param name The name of the subcommand. This is case-sensitive. + * @param handler The handler that will be called to handle subcommands with + * the given name. + * @throws UnsupportedOperationException If this handler was initialized + * with an unmodifiable map of subcommand handlers. + */ + 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. This is case-sensitive. + * @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 the case where the main command is called without any subcommand. + * @param interaction The event. + * @return The reply action that is sent to the user. + */ + protected InteractionImmediateResponseBuilder handleNonSubcommand(SlashCommandInteraction interaction) { + return Responses.warning(interaction, "Missing Subcommand", "Please specify a subcommand."); + } + + /** + * Handles a slash command interaction. + * + * @param interaction The interaction. + * @return An immediate response to the interaction. + * @throws ResponseException If an error occurs while handling the event. + */ + @Override + public InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) throws ResponseException { + var subCommandOption = interaction.getOptionByIndex(0); + if (subCommandOption.isPresent() && subCommandOption.get().isSubcommandOrGroup()) { + var firstOption = subCommandOption.get(); + // TODO: Implement some way of handling subcommand groups! For now javacord is quite scuffed in that regard. +// SlashCommandHandler groupHandler = this.getSubcommandGroupHandlers().get(firstOption.getName()); +// if (groupHandler != null) return groupHandler.handle(interaction); + SlashCommandHandler subcommandHandler = this.getSubcommandHandlers().get(firstOption.getName()); + if (subcommandHandler != null) return subcommandHandler.handle(interaction); + } + return handleNonSubcommand(interaction); + } +} diff --git a/src/main/java/net/javadiscord/javabot2/command/SlashCommandListener.java b/src/main/java/net/javadiscord/javabot2/command/SlashCommandListener.java index 1226cf9..a3ca949 100644 --- a/src/main/java/net/javadiscord/javabot2/command/SlashCommandListener.java +++ b/src/main/java/net/javadiscord/javabot2/command/SlashCommandListener.java @@ -39,7 +39,7 @@ public SlashCommandListener(DiscordApi api, boolean sendUpdate, String... resour this.commandHandlers = new HashMap<>(); registerSlashCommands(api, resources) .thenAcceptAsync(commandHandlers::putAll) - .thenRun(() -> log.info("Registered all slash commands.")); + .thenRun(() -> log.info("Registered and updated all slash commands.")); } else { this.commandHandlers = initializeHandlers(CommandDataLoader.load(resources)); log.info("Registered all slash commands."); @@ -74,6 +74,7 @@ private CompletableFuture> registerSlashCommand Map nameToId = new HashMap<>(); for (var slashCommand : slashCommands) { nameToId.put(slashCommand.getName(), slashCommand.getId()); + log.info("Updated slash command {}.", slashCommand.getName()); } return updatePermissions(api, commandConfigs, nameToId) .thenRun(() -> log.info("Updated permissions for all slash commands.")) diff --git a/src/main/java/net/javadiscord/javabot2/command/data/CommandDataLoader.java b/src/main/java/net/javadiscord/javabot2/command/data/CommandDataLoader.java index 9c33551..cc52ba6 100644 --- a/src/main/java/net/javadiscord/javabot2/command/data/CommandDataLoader.java +++ b/src/main/java/net/javadiscord/javabot2/command/data/CommandDataLoader.java @@ -30,7 +30,7 @@ public static CommandConfig[] load(String... resources) { continue; } CommandConfig[] cs = yaml.loadAs(is, CommandConfig[].class); - commands.addAll(Arrays.stream(cs).toList()); + if (cs != null) commands.addAll(Arrays.stream(cs).toList()); } return commands.toArray(new CommandConfig[0]); } diff --git a/src/main/java/net/javadiscord/javabot2/command/data/OptionConfig.java b/src/main/java/net/javadiscord/javabot2/command/data/OptionConfig.java index 37268ce..5576875 100644 --- a/src/main/java/net/javadiscord/javabot2/command/data/OptionConfig.java +++ b/src/main/java/net/javadiscord/javabot2/command/data/OptionConfig.java @@ -24,7 +24,7 @@ public class OptionConfig { */ public SlashCommandOptionBuilder toData() { var builder = new SlashCommandOptionBuilder() - .setType(SlashCommandOptionType.valueOf(this.type.toUpperCase())) + .setType(SlashCommandOptionType.valueOf(this.type.trim().toUpperCase())) .setName(this.name) .setDescription(this.description) .setRequired(this.required); diff --git a/src/main/java/net/javadiscord/javabot2/db/ConnectionFunction.java b/src/main/java/net/javadiscord/javabot2/db/ConnectionFunction.java new file mode 100644 index 0000000..14558be --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/db/ConnectionFunction.java @@ -0,0 +1,9 @@ +package net.javadiscord.javabot2.db; + +import java.sql.Connection; +import java.sql.SQLException; + +@FunctionalInterface +public interface ConnectionFunction { + T apply(Connection c) throws SQLException; +} diff --git a/src/main/java/net/javadiscord/javabot2/db/DbActions.java b/src/main/java/net/javadiscord/javabot2/db/DbActions.java new file mode 100644 index 0000000..cd4364a --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/db/DbActions.java @@ -0,0 +1,70 @@ +package net.javadiscord.javabot2.db; + +import net.javadiscord.javabot2.Bot; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** + * Utility that provides some convenience methods for performing database + * actions. + */ +public class DbActions { + // Hide the constructor. + private DbActions () {} + + /** + * Does an asynchronous database action using the bot's async pool. + * @param consumer The consumer that will use a connection. + * @return A future that completes when the action is complete. + */ + public static CompletableFuture doAction(ConnectionConsumer consumer) { + CompletableFuture future = new CompletableFuture<>(); + Bot.asyncPool.submit(() -> { + try (var c = Bot.hikariDataSource.getConnection()) { + consumer.consume(c); + future.complete(null); + } catch (SQLException e) { + future.completeExceptionally(e); + } + }); + return future; + } + + /** + * Does an asynchronous database action using the bot's async pool, and + * wraps access to the connection behind a data access object that can be + * built using the provided dao constructor. + * @param daoConstructor A function to build a DAO using a connection. + * @param consumer The consumer that does something with the DAO. + * @param The type of data access object. Usually some kind of repository. + * @return A future that completes when the action is complete. + */ + public static CompletableFuture doDaoAction(Function daoConstructor, DaoConsumer consumer) { + CompletableFuture future = new CompletableFuture<>(); + Bot.asyncPool.submit(() -> { + try (var c = Bot.hikariDataSource.getConnection()) { + var dao = daoConstructor.apply(c); + consumer.consume(dao); + future.complete(null); + } catch (SQLException e) { + future.completeExceptionally(e); + } + }); + return future; + } + + public static CompletableFuture doAction(ConnectionFunction function) { + CompletableFuture future = new CompletableFuture<>(); + Bot.asyncPool.submit(() -> { + try (var c = Bot.hikariDataSource.getConnection()) { + future.complete(function.apply(c)); + } catch (SQLException e) { + future.completeExceptionally(e); + } + }); + return future; + } +} diff --git a/src/main/java/net/javadiscord/javabot2/db/DbHelper.java b/src/main/java/net/javadiscord/javabot2/db/DbHelper.java index b023939..f17e7a7 100644 --- a/src/main/java/net/javadiscord/javabot2/db/DbHelper.java +++ b/src/main/java/net/javadiscord/javabot2/db/DbHelper.java @@ -3,7 +3,6 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import lombok.extern.slf4j.Slf4j; -import net.javadiscord.javabot2.Bot; import net.javadiscord.javabot2.config.BotConfig; import org.h2.tools.Server; @@ -11,11 +10,8 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; -import java.util.concurrent.CompletableFuture; -import java.util.function.Function; import java.util.regex.Pattern; /** @@ -65,47 +61,6 @@ public static HikariDataSource initDataSource(BotConfig config) { return ds; } - /** - * Does an asynchronous database action using the bot's async pool. - * @param consumer The consumer that will use a connection. - * @return A future that completes when the action is complete. - */ - public static CompletableFuture doDbAction(ConnectionConsumer consumer) { - CompletableFuture future = new CompletableFuture<>(); - Bot.asyncPool.submit(() -> { - try (var c = Bot.hikariDataSource.getConnection()) { - consumer.consume(c); - future.complete(null); - } catch (SQLException e) { - future.completeExceptionally(e); - } - }); - return future; - } - - /** - * Does an asynchronous database action using the bot's async pool, and - * wraps access to the connection behind a data access object that can be - * built using the provided dao constructor. - * @param daoConstructor A function to build a DAO using a connection. - * @param consumer The consumer that does something with the DAO. - * @param The type of data access object. Usually some kind of repository. - * @return A future that completes when the action is complete. - */ - public static CompletableFuture doDaoAction(Function daoConstructor, DaoConsumer consumer) { - CompletableFuture future = new CompletableFuture<>(); - Bot.asyncPool.submit(() -> { - try (var c = Bot.hikariDataSource.getConnection()) { - var dao = daoConstructor.apply(c); - consumer.consume(dao); - future.complete(null); - } catch (SQLException e) { - future.completeExceptionally(e); - } - }); - return future; - } - private static boolean shouldInitSchema(String jdbcUrl) { var p = Pattern.compile("jdbc:h2:tcp://localhost:\\d+/(.*)"); var m = p.matcher(jdbcUrl); diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/package-info.java b/src/main/java/net/javadiscord/javabot2/systems/activity/package-info.java new file mode 100644 index 0000000..1421cb6 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/package-info.java @@ -0,0 +1,5 @@ +/** + * The "activity" system contains commands and services pertaining to events and + * activities that the server manages. + */ +package net.javadiscord.javabot2.systems.activity; \ No newline at end of file diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/AddQuestionSubcommand.java b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/AddQuestionSubcommand.java new file mode 100644 index 0000000..83cdf2c --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/AddQuestionSubcommand.java @@ -0,0 +1,27 @@ +package net.javadiscord.javabot2.systems.activity.qotw; + +import net.javadiscord.javabot2.command.ResponseException; +import net.javadiscord.javabot2.command.Responses; +import net.javadiscord.javabot2.command.SlashCommandHandler; +import org.javacord.api.interaction.SlashCommandInteraction; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +public class AddQuestionSubcommand implements SlashCommandHandler { + @Override + public InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) throws ResponseException { + String question = interaction.getOptionStringValueByName("question") + .orElseThrow(ResponseException.warning("Missing required question.")); + int priority = (int) interaction.getOptionLongValueByName("priority") + .orElse(0L).longValue(); + var service = new QOTWService(); + service.saveNewQuestion(interaction.getUser(), question, priority) + .thenAcceptAsync(q -> { + interaction.getChannel().orElseThrow().sendMessage("Question **" + q.getId() + "** has been added to the queue."); + }) + .exceptionallyAsync(throwable -> { + interaction.getChannel().orElseThrow().sendMessage("An error occurred and the question could not be added to the queue."); + return null; + }); + return Responses.success(interaction, "Question Added", "Your question has been added to the QOTW queue."); + } +} diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/ListAnswersSubcommand.java b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/ListAnswersSubcommand.java new file mode 100644 index 0000000..229f96b --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/ListAnswersSubcommand.java @@ -0,0 +1,20 @@ +package net.javadiscord.javabot2.systems.activity.qotw; + +import net.javadiscord.javabot2.command.ResponseException; +import net.javadiscord.javabot2.command.SlashCommandHandler; +import org.javacord.api.interaction.SlashCommandInteraction; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +public class ListAnswersSubcommand implements SlashCommandHandler { + /** + * Handles a slash command interaction. + * + * @param interaction The interaction. + * @return An immediate response to the interaction. + * @throws ResponseException If an error occurs while handling the event. + */ + @Override + public InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) throws ResponseException { + return null; + } +} diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/ListQuestionsSubcommand.java b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/ListQuestionsSubcommand.java new file mode 100644 index 0000000..3ba51d7 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/ListQuestionsSubcommand.java @@ -0,0 +1,20 @@ +package net.javadiscord.javabot2.systems.activity.qotw; + +import net.javadiscord.javabot2.command.ResponseException; +import net.javadiscord.javabot2.command.SlashCommandHandler; +import org.javacord.api.interaction.SlashCommandInteraction; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +public class ListQuestionsSubcommand implements SlashCommandHandler { + /** + * Handles a slash command interaction. + * + * @param interaction The interaction. + * @return An immediate response to the interaction. + * @throws ResponseException If an error occurs while handling the event. + */ + @Override + public InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) throws ResponseException { + return null; + } +} diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/QOTWCommand.java b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/QOTWCommand.java new file mode 100644 index 0000000..20a2452 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/QOTWCommand.java @@ -0,0 +1,12 @@ +package net.javadiscord.javabot2.systems.activity.qotw; + +import net.javadiscord.javabot2.command.DelegatingCommandHandler; + +public class QOTWCommand extends DelegatingCommandHandler { + public QOTWCommand() { + addSubcommand("add-question", new AddQuestionSubcommand()); + addSubcommand("remove-question", new RemoveQuestionSubcommand()); + addSubcommand("list-questions", new ListQuestionsSubcommand()); + addSubcommand("list-answers", new ListAnswersSubcommand()); + } +} diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/QOTWService.java b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/QOTWService.java new file mode 100644 index 0000000..c1651db --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/QOTWService.java @@ -0,0 +1,23 @@ +package net.javadiscord.javabot2.systems.activity.qotw; + +import net.javadiscord.javabot2.db.DbActions; +import net.javadiscord.javabot2.systems.activity.qotw.dao.QuestionRepository; +import net.javadiscord.javabot2.systems.activity.qotw.model.Question; +import org.javacord.api.entity.user.User; + +import java.util.concurrent.CompletableFuture; + +public class QOTWService { + /** + * + * @param createdBy + * @param questionStr + * @param priority + */ + public CompletableFuture saveNewQuestion(User createdBy, String questionStr, int priority) { + return DbActions.doAction(c -> { + var dao = new QuestionRepository(c); + return dao.insert(new Question(createdBy.getId(), questionStr, priority)); + }); + } +} diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/RemoveQuestionSubcommand.java b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/RemoveQuestionSubcommand.java new file mode 100644 index 0000000..ac1faeb --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/RemoveQuestionSubcommand.java @@ -0,0 +1,20 @@ +package net.javadiscord.javabot2.systems.activity.qotw; + +import net.javadiscord.javabot2.command.ResponseException; +import net.javadiscord.javabot2.command.SlashCommandHandler; +import org.javacord.api.interaction.SlashCommandInteraction; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +public class RemoveQuestionSubcommand implements SlashCommandHandler { + /** + * Handles a slash command interaction. + * + * @param interaction The interaction. + * @return An immediate response to the interaction. + * @throws ResponseException If an error occurs while handling the event. + */ + @Override + public InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) throws ResponseException { + return null; + } +} diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/dao/QuestionRepository.java b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/dao/QuestionRepository.java new file mode 100644 index 0000000..462aece --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/dao/QuestionRepository.java @@ -0,0 +1,53 @@ +package net.javadiscord.javabot2.systems.activity.qotw.dao; + +import lombok.RequiredArgsConstructor; +import net.javadiscord.javabot2.systems.activity.qotw.model.Question; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Optional; + +@RequiredArgsConstructor +public class QuestionRepository { + private final Connection con; + + public Question insert(Question question) throws SQLException { + try (var s = con.prepareStatement(""" + INSERT INTO qotw_question(created_by, question, priority) VALUES (?, ?, ?); + """, Statement.RETURN_GENERATED_KEYS)) { + s.setLong(1, question.getCreatedBy()); + s.setString(2, question.getQuestion()); + s.setInt(3, question.getPriority()); + s.executeUpdate(); + var rs = s.getGeneratedKeys(); + if (!rs.next()) throw new SQLException("No generated keys returned."); + long id = rs.getLong(1); + return findById(id).orElseThrow(); + } + } + + public Optional findById(long id) throws SQLException { + try (var s = con.prepareStatement("SELECT * FROM qotw_question WHERE id = ?")) { + s.setLong(1, id); + var rs = s.executeQuery(); + if (rs.next()) return Optional.of(read(rs)); + } + return Optional.empty(); + } + + private Question read(ResultSet rs) throws SQLException { + Question q = new Question(); + q.setId(rs.getLong("id")); + q.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); + q.setCreatedBy(rs.getLong("created_by")); + q.setQuestion(rs.getString("question")); + q.setPriority(rs.getInt("priority")); + q.setActive(rs.getBoolean("active")); + q.setUsed(rs.getBoolean("used")); + var ts = rs.getTimestamp("activated_at"); + q.setActivatedAt(ts == null ? null : ts.toLocalDateTime()); + return q; + } +} diff --git a/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/model/Question.java b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/model/Question.java new file mode 100644 index 0000000..68eb70d --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/activity/qotw/model/Question.java @@ -0,0 +1,25 @@ +package net.javadiscord.javabot2.systems.activity.qotw.model; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class Question { + private Long id; + private long createdBy; + private LocalDateTime createdAt; + private String question; + private int priority; + private boolean active; + private LocalDateTime activatedAt; + private boolean used; + + public Question(long createdBy, String question, int priority) { + this.createdBy = createdBy; + this.question = question; + this.priority = priority; + } +} diff --git a/src/main/java/net/javadiscord/javabot2/systems/moderation/ModerationService.java b/src/main/java/net/javadiscord/javabot2/systems/moderation/ModerationService.java index 0c5ba3e..94c6e6c 100644 --- a/src/main/java/net/javadiscord/javabot2/systems/moderation/ModerationService.java +++ b/src/main/java/net/javadiscord/javabot2/systems/moderation/ModerationService.java @@ -5,7 +5,7 @@ import net.javadiscord.javabot2.Bot; import net.javadiscord.javabot2.command.ResponseException; import net.javadiscord.javabot2.config.guild.ModerationConfig; -import net.javadiscord.javabot2.db.DbHelper; +import net.javadiscord.javabot2.db.DbActions; import net.javadiscord.javabot2.systems.moderation.dao.MuteRepository; import net.javadiscord.javabot2.systems.moderation.dao.WarnRepository; import net.javadiscord.javabot2.systems.moderation.model.Mute; @@ -73,8 +73,7 @@ public ModerationService(SlashCommandInteraction interaction) throws ResponseExc * @return A future that completes when all warn operations are complete. */ public CompletableFuture warn(User user, WarnSeverity severity, String reason, User warnedBy, ServerTextChannel channel, boolean quiet) { - return DbHelper.doDbAction(con -> { - var repo = new WarnRepository(con); + return DbActions.doDaoAction(WarnRepository::new, repo -> { var warn = repo.insert(new Warn(user.getId(), warnedBy.getId(), severity, reason)); LocalDateTime cutoff = LocalDateTime.now().minusDays(config.getWarnTimeoutDays()); int totalWeight = repo.getTotalSeverityWeight(user.getId(), cutoff); @@ -97,7 +96,7 @@ public CompletableFuture warn(User user, WarnSeverity severity, String rea * @return A future that completes when the warns have been cleared. */ public CompletableFuture clearWarns(User user, User clearedBy) { - return DbHelper.doDaoAction(WarnRepository::new, dao -> { + return DbActions.doDaoAction(WarnRepository::new, dao -> { dao.discardAll(user.getId()); var embed = buildClearWarnsEmbed(user, clearedBy); user.openPrivateChannel().thenAcceptAsync(pc -> pc.sendMessage(embed)); @@ -142,7 +141,7 @@ public CompletableFuture ban(User user, String reason, User bannedBy, Serv * @return A future that completes when muting is done. */ public CompletableFuture mute(User user, String reason, User mutedBy, Duration duration, ServerTextChannel channel, boolean quiet) { - return DbHelper.doDbAction(con -> { + return DbActions.doAction(con -> { con.setAutoCommit(false); var repo = new MuteRepository(con); var embed = buildMuteEmbed(user, reason, duration, mutedBy); @@ -193,7 +192,7 @@ public CompletableFuture mute(User user, String reason, User mutedBy, Dura * @return A future that completes when the user is unmuted. */ public CompletableFuture unmute(User user, User unmutedBy) { - return DbHelper.doDbAction(con -> { + return DbActions.doAction(con -> { var repo = new MuteRepository(con); repo.discardAllActive(user.getId()); user.removeRole(config.getMuteRole()); @@ -208,7 +207,7 @@ public CompletableFuture unmute(User user, User unmutedBy) { * @return A future that completes when all expired mutes have been processed. */ public CompletableFuture unmuteExpired() { - return DbHelper.doDbAction(con -> { + return DbActions.doAction(con -> { con.setAutoCommit(false); var repo = new MuteRepository(con); ServerUpdater updater = new ServerUpdater(config.getGuild()); diff --git a/src/main/resources/commands/activity.yaml b/src/main/resources/commands/activity.yaml new file mode 100644 index 0000000..6723732 --- /dev/null +++ b/src/main/resources/commands/activity.yaml @@ -0,0 +1,40 @@ +- name: qotw + description: Manage the Question of the Week information. + handler: net.javadiscord.javabot2.systems.activity.qotw.QOTWCommand + enabledByDefault: false + privileges: + - type: ROLE + id: moderation.staffRoleId + subCommands: + - name: add-question + description: Add a new question to the QOTW queue. + options: + - name: question + description: The question to ask. + type: STRING + required: true + - name: priority + description: The priority of this question. Defaults to 0. + type: INTEGER + required: false + - name: list-questions + description: Lists all queued QOTW questions. + options: + - name: include-used + description: Should used questions be included in the results? Defaults to False. + type: BOOLEAN + required: false + - name: remove-question + description: Removes a question from the QOTW queue. Only unused and inactive questions can be removed. + options: + - name: question-id + description: The id of the question to remove. Find ids using the `list` command. + type: INTEGER + required: true + - name: list-answers + description: Lists all answers for a given question. + options: + - name: question-id + description: The id of the question to find answers for. + type: INTEGER + required: true diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index 8ecc763..1b7e596 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -1,3 +1,5 @@ +-- Moderation Tables + CREATE TABLE warn ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, @@ -18,3 +20,29 @@ CREATE TABLE mute ( ends_at TIMESTAMP(0) NOT NULL, discarded BOOL NOT NULL DEFAULT FALSE ); + +-- Activity Tables + +CREATE TABLE qotw_question ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + created_by BIGINT NOT NULL, + created_at TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), + question VARCHAR(1024) NOT NULL, + priority INT NOT NULL DEFAULT 0, + active BOOL NOT NULL DEFAULT FALSE, + activated_at TIMESTAMP(0) NULL DEFAULT NULL, + used BOOL NOT NULL DEFAULT FALSE +); + +CREATE TABLE qotw_answer ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + created_by BIGINT NOT NULL, + created_at TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), + answer CLOB NOT NULL, + question_id BIGINT NOT NULL, + accepted BOOL NULL DEFAULT NULL, + FOREIGN KEY (question_id) REFERENCES qotw_question(id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +