From 2eb19712e0da4055d73e890cef6045533242a194 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Tue, 9 Jan 2024 18:13:47 +0100 Subject: [PATCH 1/4] restrict closing of posts by non-original posters --- .../javabot/systems/help/HelpListener.java | 11 ++++------- .../javabot/systems/help/HelpManager.java | 14 ++++++++++++++ .../systems/help/commands/UnreserveCommand.java | 7 ++++++- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/discordjug/javabot/systems/help/HelpListener.java b/src/main/java/net/discordjug/javabot/systems/help/HelpListener.java index d59aa949c..bef125a9c 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/HelpListener.java +++ b/src/main/java/net/discordjug/javabot/systems/help/HelpListener.java @@ -245,7 +245,6 @@ private boolean isInvalidHelpForumChannel(@NotNull ForumChannel forum) { private void handleHelpThanksInteraction(@NotNull ButtonInteractionEvent event, @NotNull HelpManager manager, String @NotNull [] id) { ThreadChannel post = manager.getPostThread(); - HelpConfig config = botConfig.get(event.getGuild()).getHelpConfig(); if (event.getUser().getIdLong() != post.getOwnerIdLong()) { Responses.warning( event, @@ -301,16 +300,14 @@ private void handleReplyGuidelines(@NotNull IReplyCallback callback, @NotNull Fo } private void handlePostClose(ButtonInteractionEvent event, @NotNull HelpManager manager) { - if (manager.isForumEligibleToBeUnreserved(event)) { - manager.close(event, event.getUser().getIdLong() == manager.getPostThread() - .getOwnerIdLong(), null); - } else { + if (event.getUser().getIdLong() != manager.getPostThread().getOwnerIdLong()) { Responses.warning( event, - "Could not close this post", - "You're not allowed to close this post." + "Sorry, but only the original poster can close this post using these buttons." ) .queue(); + return; } + manager.close(event, true, null); } } \ No newline at end of file diff --git a/src/main/java/net/discordjug/javabot/systems/help/HelpManager.java b/src/main/java/net/discordjug/javabot/systems/help/HelpManager.java index ca21a5abe..217f4406a 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/HelpManager.java +++ b/src/main/java/net/discordjug/javabot/systems/help/HelpManager.java @@ -117,9 +117,23 @@ public void close(IReplyCallback callback, boolean withHelpers, @Nullable String .queue(s -> postThread.getManager().setLocked(true).setArchived(true).queue()); if (callback.getMember().getIdLong() != postThread.getOwnerIdLong() && Boolean.parseBoolean(preferenceService.getOrCreate(postThread.getOwnerIdLong(), Preference.PRIVATE_CLOSE_NOTIFICATIONS).getState())) { + postThread.getOwner().getUser().openPrivateChannel() .flatMap(c -> createDMCloseInfoEmbed(callback.getMember(), postThread, reason, c)) .queue(success -> {}, failure -> {}); + + botConfig.get(callback.getGuild()) + .getModerationConfig() + .getLogChannel() + .sendMessageEmbeds(new EmbedBuilder() + .setTitle("Post closed by non-original poster") + .setDescription("The post " + postThread.getAsMention() + + " has been closed by " + callback.getMember().getAsMention() + ".\n\n" + + "[Post link](" + postThread.getJumpUrl() + ")") + .addField("Reason", reason, false) + .setAuthor(callback.getMember().getEffectiveName(), null, callback.getMember().getEffectiveAvatarUrl()) + .build()) + .queue(); } } diff --git a/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java b/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java index 95b7225a6..6c53a97c5 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java +++ b/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java @@ -62,9 +62,14 @@ public void execute(@NotNull SlashCommandInteractionEvent event) { } HelpManager manager = new HelpManager(postThread, dbActions, botConfig, helpAccountRepository, helpTransactionRepository, preferenceService); if (manager.isForumEligibleToBeUnreserved(event.getInteraction())) { + String reason = event.getOption("reason", null, OptionMapping::getAsString); + if (event.getUser().getIdLong() != postThread.getOwnerIdLong() && reason == null) { + Responses.warning(event, "Could not close this post", "Closing a post of another user requires a reason to be set.").queue(); + return; + } manager.close(event, event.getUser().getIdLong() == manager.getPostThread().getOwnerIdLong(), - event.getOption("reason", null, OptionMapping::getAsString)); + reason); } else { Responses.warning(event, "Could not close this post", "You're not allowed to close this post.").queue(); } From eb002e5bcc352aa234f9d110332085570d0ca03d Mon Sep 17 00:00:00 2001 From: danthe1st Date: Sat, 13 Jan 2024 11:40:42 +0100 Subject: [PATCH 2/4] add modal for reason when closing --- src/main/java/net/discordjug/javabot/Bot.java | 10 ++- .../help/commands/UnreserveCommand.java | 90 +++++++++++++++---- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/main/java/net/discordjug/javabot/Bot.java b/src/main/java/net/discordjug/javabot/Bot.java index 8b75b1c85..29f478169 100644 --- a/src/main/java/net/discordjug/javabot/Bot.java +++ b/src/main/java/net/discordjug/javabot/Bot.java @@ -112,10 +112,12 @@ private void registerComponentHandlers(@NotNull ApplicationContext ctx) { List> stringSelectMappings = new ArrayList<>(); for (Object handler : interactionHandlers.values()) { AutoDetectableComponentHandler annotation = handler.getClass().getAnnotation(AutoDetectableComponentHandler.class); - String[] keys = annotation.value(); - addComponentHandler(buttonMappings, keys, handler, ButtonHandler.class); - addComponentHandler(modalMappings, keys, handler, ModalHandler.class); - addComponentHandler(stringSelectMappings, keys, handler, StringSelectMenuHandler.class); + if (annotation != null) {//superclasses are annotated, ignore + String[] keys = annotation.value(); + addComponentHandler(buttonMappings, keys, handler, ButtonHandler.class); + addComponentHandler(modalMappings, keys, handler, ModalHandler.class); + addComponentHandler(stringSelectMappings, keys, handler, StringSelectMenuHandler.class); + } } dih4jda.addButtonMappings(buttonMappings.toArray(IdMapping[]::new)); dih4jda.addModalMappings(modalMappings.toArray(IdMapping[]::new)); diff --git a/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java b/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java index 6c53a97c5..746192467 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java +++ b/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java @@ -1,28 +1,45 @@ package net.discordjug.javabot.systems.help.commands; +import java.util.List; + +import org.jetbrains.annotations.NotNull; + +import net.discordjug.javabot.annotations.AutoDetectableComponentHandler; import net.discordjug.javabot.data.config.BotConfig; import net.discordjug.javabot.data.h2db.DbActions; import net.discordjug.javabot.systems.help.HelpManager; import net.discordjug.javabot.systems.help.dao.HelpAccountRepository; import net.discordjug.javabot.systems.help.dao.HelpTransactionRepository; import net.discordjug.javabot.systems.user_preferences.UserPreferenceService; +import net.discordjug.javabot.util.ExceptionLogger; import net.discordjug.javabot.util.Responses; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import net.dv8tion.jda.api.interactions.commands.CommandInteraction; +import net.dv8tion.jda.api.interactions.Interaction; +import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.Commands; - -import org.jetbrains.annotations.NotNull; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand; +import xyz.dynxsty.dih4jda.interactions.components.ModalHandler; +import xyz.dynxsty.dih4jda.util.ComponentIdBuilder; /** * A simple command that can be used inside reserved help channels to * immediately unreserve them, instead of waiting for a timeout. */ -public class UnreserveCommand extends SlashCommand { +@AutoDetectableComponentHandler(UnreserveCommand.UNRESERVE_ID) +public class UnreserveCommand extends SlashCommand implements ModalHandler { + private static final String UNRESERVE_ID = "unreserve"; + private static final String REASON_ID = "reason"; private final BotConfig botConfig; private final DbActions dbActions; private final HelpAccountRepository helpAccountRepository; @@ -43,40 +60,75 @@ public UnreserveCommand(BotConfig botConfig, DbActions dbActions, HelpTransactio this.helpAccountRepository = helpAccountRepository; this.helpTransactionRepository = helpTransactionRepository; this.preferenceService = preferenceService; - setCommandData(Commands.slash("unreserve", "Unreserves this post marking your question/issue as resolved.") + setCommandData(Commands.slash(UNRESERVE_ID, "Unreserves this post marking your question/issue as resolved.") .setGuildOnly(true) - .addOption(OptionType.STRING, "reason", "The reason why you're unreserving this channel", false) + .addOption(OptionType.STRING, REASON_ID, "The reason why you're unreserving this channel", false) ); } @Override public void execute(@NotNull SlashCommandInteractionEvent event) { + String reason = event.getOption(REASON_ID, null, OptionMapping::getAsString); + onCloseRequest(event, event, event.getChannel(), reason, ()->{ + TextInput reasonInput = TextInput + .create(REASON_ID, "Reason", TextInputStyle.SHORT) + .setRequiredRange(11, 100) + .setRequired(true) + .setPlaceholder("Please enter the reason you are closing this post here") + .build(); + Modal modal = Modal + .create(ComponentIdBuilder.build(UNRESERVE_ID), "Close post") + .addComponents(ActionRow.of( + reasonInput)) + .build(); + event.replyModal(modal).queue(); + }); + } + + @Override + public void handleModal(ModalInteractionEvent event, List values) { + values + .stream() + .filter(mapping -> REASON_ID.equals(mapping.getId())) + .map(mapping -> mapping.getAsString()) + .findAny() + .ifPresentOrElse(reason -> { + onCloseRequest(event, event, event.getChannel(), reason, ()->{ + Responses.error(event, "An error occured - The reason field is missing.").queue(); + ExceptionLogger.capture(new IllegalStateException("A reason was expected but not present"), getClass().getName()); + }); + }, () -> Responses.warning(event, "A reason must be provided").queue()); + + } + + private void onCloseRequest(Interaction interaction, IReplyCallback replyCallback, MessageChannelUnion channel, String reason, Runnable noReasonHandler) { + ChannelType channelType = channel.getType(); // check whether the channel type is either text or thread (possible forum post?) - if (event.getChannelType() != ChannelType.TEXT && event.getChannelType() != ChannelType.GUILD_PUBLIC_THREAD) { - replyInvalidChannel(event); + if (channelType != ChannelType.TEXT && channelType != ChannelType.GUILD_PUBLIC_THREAD) { + replyInvalidChannel(replyCallback); return; } - ThreadChannel postThread = event.getChannel().asThreadChannel(); + ThreadChannel postThread = channel.asThreadChannel(); if (postThread.getParentChannel().getType() != ChannelType.FORUM) { - replyInvalidChannel(event); + replyInvalidChannel(replyCallback); + return; } HelpManager manager = new HelpManager(postThread, dbActions, botConfig, helpAccountRepository, helpTransactionRepository, preferenceService); - if (manager.isForumEligibleToBeUnreserved(event.getInteraction())) { - String reason = event.getOption("reason", null, OptionMapping::getAsString); - if (event.getUser().getIdLong() != postThread.getOwnerIdLong() && reason == null) { - Responses.warning(event, "Could not close this post", "Closing a post of another user requires a reason to be set.").queue(); + if (manager.isForumEligibleToBeUnreserved(interaction)) { + if (replyCallback.getUser().getIdLong() != postThread.getOwnerIdLong() && reason == null) { + noReasonHandler.run(); return; } - manager.close(event, - event.getUser().getIdLong() == manager.getPostThread().getOwnerIdLong(), + manager.close(replyCallback, + replyCallback.getUser().getIdLong() == manager.getPostThread().getOwnerIdLong(), reason); } else { - Responses.warning(event, "Could not close this post", "You're not allowed to close this post.").queue(); + Responses.warning(replyCallback, "Could not close this post", "You're not allowed to close this post.").queue(); } } - private void replyInvalidChannel(CommandInteraction interaction) { - Responses.warning(interaction, "Invalid Channel", + private void replyInvalidChannel(IReplyCallback replyCallback) { + Responses.warning(replyCallback, "Invalid Channel", "This command may only be used in either the text-channel-based help system, or in our new forum help system.") .queue(); } From fd30a49d2e024146c368b228e4ecc0a1cb6a58a1 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Sat, 13 Jan 2024 11:47:38 +0100 Subject: [PATCH 3/4] make constant package private for annotation --- .../javabot/systems/help/commands/UnreserveCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java b/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java index 746192467..619f60cc2 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java +++ b/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java @@ -38,7 +38,7 @@ */ @AutoDetectableComponentHandler(UnreserveCommand.UNRESERVE_ID) public class UnreserveCommand extends SlashCommand implements ModalHandler { - private static final String UNRESERVE_ID = "unreserve"; + static final String UNRESERVE_ID = "unreserve"; private static final String REASON_ID = "reason"; private final BotConfig botConfig; private final DbActions dbActions; From 94252f37a975d759b07a14ee871bdcf94d028e39 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Sat, 13 Jan 2024 11:55:13 +0100 Subject: [PATCH 4/4] add reason size limit to command --- .../systems/help/commands/UnreserveCommand.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java b/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java index 619f60cc2..19c522df1 100644 --- a/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java +++ b/src/main/java/net/discordjug/javabot/systems/help/commands/UnreserveCommand.java @@ -39,6 +39,7 @@ @AutoDetectableComponentHandler(UnreserveCommand.UNRESERVE_ID) public class UnreserveCommand extends SlashCommand implements ModalHandler { static final String UNRESERVE_ID = "unreserve"; + private static final int MINIMUM_REASON_LENGTH = 11; private static final String REASON_ID = "reason"; private final BotConfig botConfig; private final DbActions dbActions; @@ -72,9 +73,9 @@ public void execute(@NotNull SlashCommandInteractionEvent event) { onCloseRequest(event, event, event.getChannel(), reason, ()->{ TextInput reasonInput = TextInput .create(REASON_ID, "Reason", TextInputStyle.SHORT) - .setRequiredRange(11, 100) + .setRequiredRange(MINIMUM_REASON_LENGTH, 100) .setRequired(true) - .setPlaceholder("Please enter the reason you are closing this post here") + .setPlaceholder(reason == null ? "Please enter the reason you are closing this post here" : reason) .build(); Modal modal = Modal .create(ComponentIdBuilder.build(UNRESERVE_ID), "Close post") @@ -91,13 +92,14 @@ public void handleModal(ModalInteractionEvent event, List values) .stream() .filter(mapping -> REASON_ID.equals(mapping.getId())) .map(mapping -> mapping.getAsString()) + .filter(reason -> !isReasonInvalid(reason)) .findAny() .ifPresentOrElse(reason -> { onCloseRequest(event, event, event.getChannel(), reason, ()->{ - Responses.error(event, "An error occured - The reason field is missing.").queue(); + Responses.error(event, "The provided reason is missing or not valid").queue(); ExceptionLogger.capture(new IllegalStateException("A reason was expected but not present"), getClass().getName()); }); - }, () -> Responses.warning(event, "A reason must be provided").queue()); + }, () -> Responses.warning(event, "A valid reason must be provided").queue()); } @@ -115,7 +117,7 @@ private void onCloseRequest(Interaction interaction, IReplyCallback replyCallbac } HelpManager manager = new HelpManager(postThread, dbActions, botConfig, helpAccountRepository, helpTransactionRepository, preferenceService); if (manager.isForumEligibleToBeUnreserved(interaction)) { - if (replyCallback.getUser().getIdLong() != postThread.getOwnerIdLong() && reason == null) { + if (replyCallback.getUser().getIdLong() != postThread.getOwnerIdLong() && isReasonInvalid(reason)) { noReasonHandler.run(); return; } @@ -127,6 +129,10 @@ private void onCloseRequest(Interaction interaction, IReplyCallback replyCallbac } } + private boolean isReasonInvalid(String reason) { + return reason == null || reason.length() < MINIMUM_REASON_LENGTH; + } + private void replyInvalidChannel(IReplyCallback replyCallback) { Responses.warning(replyCallback, "Invalid Channel", "This command may only be used in either the text-channel-based help system, or in our new forum help system.")