diff --git a/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatAndIndentCodeMessageContext.java b/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatAndIndentCodeMessageContext.java index 4c3bbc7c4..f355fdc01 100644 --- a/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatAndIndentCodeMessageContext.java +++ b/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatAndIndentCodeMessageContext.java @@ -30,6 +30,6 @@ public void execute(@NotNull MessageContextInteractionEvent event) { Code code = new Code(Language.JAVA, indented); - event.deferReply().queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget())); + event.deferReply(true).queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget())); } } diff --git a/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeCommand.java b/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeCommand.java index 10744eab6..ebbfecceb 100644 --- a/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeCommand.java +++ b/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeCommand.java @@ -56,7 +56,7 @@ public void execute(@NotNull SlashCommandInteractionEvent event) { String indentation = event.getOption("auto-indent","NULL",OptionMapping::getAsString); if (idOption == null) { - event.deferReply().queue(_ -> { + event.deferReply(true).queue(_ -> { event.getChannel().getHistory() .retrievePast(10) .queue(messages -> { @@ -78,7 +78,7 @@ public void execute(@NotNull SlashCommandInteractionEvent event) { return; } long messageId = idOption.getAsLong(); - event.deferReply().queue(_ -> { + event.deferReply(true).queue(_ -> { event.getChannel().retrieveMessageById(messageId).queue( target -> sendFormattedCode(event, target, language, indentation), error -> Responses.errorWithTitle(event.getHook(), "Message Not Found", "Could not retrieve the message with ID `" + messageId + "`. Make sure the message exists and is accessible.").queue()); diff --git a/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeDispatcher.java b/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeDispatcher.java index 4fc1215ac..f0b04375f 100644 --- a/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeDispatcher.java +++ b/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeDispatcher.java @@ -49,7 +49,9 @@ public static void sendCode(Code code, @Nonnull CommandInteraction event, Messag return; } - Responses.success(event.getHook(), "Success", "The formatted message is being sent to this channel.") + event.getHook().sendMessage("Your message has been formatted. If needed, you can change the language used for syntax highlighting below.") + .setEphemeral(true) + .setComponents(FormatCodeInteractionHandler.buildLanguageMenu(event.getUser().getIdLong(),messages.size())) .queue(success -> sendChunksInOrder(channel, messages, 0, target,event)); } @@ -62,38 +64,16 @@ private static void sendChunksInOrder(MessageChannel channel, List messa .setAllowedMentions(List.of()); if (index == messages.size() - 1) { - if(index == 0){ - action.setComponents(buildActionRow(target, event.getUser().getIdLong())); - } else { - action.setComponents(buildActionRow(target)); - } + action.setComponents(buildActionRow(target, event.getUser().getIdLong(), messages.size())); } - action.queue(success -> - sendChunksInOrder(channel, messages, index + 1, target, event)); + action.queue(sent -> sendChunksInOrder(channel, messages, index + 1, target, event)); } - /** - * Builds the action row placed on the last code-block message. - * - * @param target the original message linked by the "View Original" button - * @return an action row containing the "View Original" link button - */ - @Contract("_ -> new") - static @NotNull ActionRow buildActionRow(@NotNull Message target) { - return ActionRow.of(Button.link(target.getJumpUrl(), "View Original")); - } - - /** - * Builds the action row placed on the file-upload message: a delete button and a "View Original" link. - * - * @param target the original message linked by the "View Original" button - * @param requesterId the id of the user permitted to delete the message - * @return an action row containing the delete and "View Original" buttons - */ - @Contract("_,_ -> new") - static @NotNull ActionRow buildActionRow(@NotNull Message target, long requesterId) { - return ActionRow.of(InteractionUtils.createDeleteButton(requesterId), + @Contract("_,_,_ -> new") + static @NotNull ActionRow buildActionRow(@NotNull Message target, long requesterId, int total) { + return ActionRow.of( + FormatCodeInteractionHandler.createDeleteAllButton(requesterId, total), Button.link(target.getJumpUrl(), "View Original")); } } diff --git a/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeInteractionHandler.java b/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeInteractionHandler.java new file mode 100644 index 000000000..fa5eb5db2 --- /dev/null +++ b/src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeInteractionHandler.java @@ -0,0 +1,166 @@ +package net.discordjug.javabot.systems.user_commands.format_code; + +import lombok.RequiredArgsConstructor; +import net.discordjug.javabot.annotations.AutoDetectableComponentHandler; +import net.discordjug.javabot.data.config.BotConfig; +import net.discordjug.javabot.util.Checks; +import net.discordjug.javabot.util.Responses; +import net.dv8tion.jda.api.components.actionrow.ActionRow; +import net.dv8tion.jda.api.components.buttons.Button; +import net.dv8tion.jda.api.components.selections.SelectOption; +import net.dv8tion.jda.api.components.selections.StringSelectMenu; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import java.util.stream.Stream; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; +import org.jspecify.annotations.NonNull; +import xyz.dynxsty.dih4jda.interactions.components.ButtonHandler; +import xyz.dynxsty.dih4jda.interactions.components.StringSelectMenuHandler; +import xyz.dynxsty.dih4jda.util.ComponentIdBuilder; + +import java.util.Arrays; +import java.util.List; + +/** + * Handles the interactive components on formatted code blocks: the delete-all button and the + * language-selection dropdown. Both act on every message of a (possibly multi-message) block, + * which is resolved via {@link LinkedMessages}. + */ +@AutoDetectableComponentHandler(FormatCodeInteractionHandler.COMPONENT_ID) +@RequiredArgsConstructor +public class FormatCodeInteractionHandler implements ButtonHandler, StringSelectMenuHandler { + static final String COMPONENT_ID = "format-code"; + private final BotConfig botConfig; + + /** + * Builds the delete-all button placed on the last message of a code block. + * + * @param requesterID the id of the user allowed to delete the block + * @param total the number of messages making up the block + * @return the delete-all button + */ + public static Button createDeleteAllButton(long requesterID, int total){ + return Button.secondary(ComponentIdBuilder.build(COMPONENT_ID,requesterID,total),"\uD83D\uDDD1\uFE0F"); + } + + private static StringSelectMenu languageMenu(String customId) { + return StringSelectMenu.create(customId) + .setPlaceholder("Change language") + .addOptions(Arrays.stream(Language.values()) + .filter(language -> language != Language.UNKNOWN) + .map(language -> SelectOption.of(language.getDisplayName(), language.name())) + .toList()) + .build(); + } + + /** + * Builds the language-selection dropdown row for a code block. + * + * @param requesterId the id of the user allowed to change the language + * @param total the number of messages making up the block + * @return an action row containing the language dropdown + */ + public static ActionRow buildLanguageMenu(long requesterId, int total) { + return ActionRow.of(languageMenu(ComponentIdBuilder.build(COMPONENT_ID, requesterId, total))); + } + + @Override + public void handleButton(ButtonInteractionEvent event, @NonNull Button button) { + String[] id = ComponentIdBuilder.split(event.getComponentId()); + long requesterId = Long.parseLong(id[1]); + + + Member member = event.getMember(); + if (member == null) { + Responses.error(event, "This button may only be used inside a server.").queue(); + return; + } + if (!canManage(member, requesterId)) { + Responses.errorWithTitle(event, "Access Denied", "You are not authorized to perform this action.").queue(); + return; + } + + event.deferEdit().queue(); + + LinkedMessages.resolveBefore(event.getMessage(), Integer.parseInt(id[2]), + messages -> event.getChannel().purgeMessages(messages), + () -> Responses.error(event.getHook(),"Could not delete the code block").queue()); + } + + @Override + public void handleStringSelectMenu(@NonNull StringSelectInteractionEvent event, @NonNull List values) { + String[] id = ComponentIdBuilder.split(event.getComponentId()); + long requesterId = Long.parseLong(id[1]); + + Member member = event.getMember(); + if (member == null) { + Responses.error(event, "This menu may only be used inside a server.").queue(); + return; + } + if (!canManage(member, requesterId)) { + Responses.errorWithTitle(event, "Access Denied", "You are not authorized to perform this action.").queue(); + return; + } + + Language language = Language.fromString(values.getFirst()); + event.deferEdit().queue(); + + Runnable onError = () -> Responses.error(event.getHook(), + "Could not update the code block — it may have changed or no longer be yours.").queue(); + LinkedMessages.resolveAfter(event.getMessage(), Integer.parseInt(id[2]), + messages -> { + if (ownsBlock(messages, event.getUser().getIdLong())) { + messages.forEach(message -> + message.editMessage(withLanguage(message.getContentRaw(), language)).queue()); + } else { + onError.run(); + } + }, + onError); + } + + private static boolean ownsBlock(List messages, long userId) { + Message last = messages.get(messages.size() - 1); + boolean lastIsUsers = buttonsOf(last).anyMatch(button -> isDeleteButtonOf(button, userId)); + boolean noOtherHasButton = messages.subList(0, messages.size() - 1).stream() + .flatMap(FormatCodeInteractionHandler::buttonsOf) + .noneMatch(FormatCodeInteractionHandler::isDeleteButton); + return lastIsUsers && noOtherHasButton; + } + + private static Stream