| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,6 +30,6 @@ public void execute(@NotNull MessageContextInteractionEvent event) { | |||
| 30 | 30 | ||
| 31 | 31 | Code code = new Code(Language.JAVA, indented); | |
| 32 | 32 | ||
| 33 | - event.deferReply().queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget())); | ||
| 33 | + event.deferReply(true).queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget())); | ||
| 34 | 34 | } | |
| 35 | 35 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -56,7 +56,7 @@ public void execute(@NotNull SlashCommandInteractionEvent event) { | |||
| 56 | 56 | String indentation = event.getOption("auto-indent","NULL",OptionMapping::getAsString); | |
| 57 | 57 | ||
| 58 | 58 | if (idOption == null) { | |
| 59 | - event.deferReply().queue(_ -> { | ||
| 59 | + event.deferReply(true).queue(_ -> { | ||
| 60 | 60 | event.getChannel().getHistory() | |
| 61 | 61 | .retrievePast(10) | |
| 62 | 62 | .queue(messages -> { | |
@@ -78,7 +78,7 @@ public void execute(@NotNull SlashCommandInteractionEvent event) { | |||
| 78 | 78 | return; | |
| 79 | 79 | } | |
| 80 | 80 | long messageId = idOption.getAsLong(); | |
| 81 | - event.deferReply().queue(_ -> { | ||
| 81 | + event.deferReply(true).queue(_ -> { | ||
| 82 | 82 | event.getChannel().retrieveMessageById(messageId).queue( | |
| 83 | 83 | target -> sendFormattedCode(event, target, language, indentation), | |
| 84 | 84 | 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()); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,6 +6,7 @@ | |||
| 6 | 6 | import net.dv8tion.jda.api.entities.Message; | |
| 7 | 7 | import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; | |
| 8 | 8 | import net.dv8tion.jda.api.interactions.commands.CommandInteraction; | |
| 9 | + import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; | ||
| 9 | 10 | import org.jetbrains.annotations.Contract; | |
| 10 | 11 | import org.jetbrains.annotations.NotNull; | |
| 11 | 12 | ||
@@ -33,9 +34,9 @@ class FormatCodeDispatcher { | |||
| 33 | 34 | * @param target the original message the code came from, used for the channel and the | |
| 34 | 35 | * "View Original" / delete buttons | |
| 35 | 36 | */ | |
| 36 | - public static void sendCode(Code code, @Nonnull CommandInteraction event, Message target){ | ||
| 37 | + public static void sendCode(Code code, @Nonnull CommandInteraction event, Message target) { | ||
| 37 | 38 | if (code.getContent().isBlank()) { | |
| 38 | - Responses.errorWithTitle(event.getHook(), "404 Code not found","There is no code to format in that message.").queue(); | ||
| 39 | + Responses.errorWithTitle(event.getHook(), "404 Code not found", "There is no code to format in that message.").queue(); | ||
| 39 | 40 | return; | |
| 40 | 41 | } | |
| 41 | 42 | ||
@@ -44,56 +45,50 @@ public static void sendCode(Code code, @Nonnull CommandInteraction event, Messag | |||
| 44 | 45 | MessageChannel channel = target.getChannel(); | |
| 45 | 46 | ||
| 46 | 47 | if (messages.size() > MAX_MESSAGES) { | |
| 47 | - Responses.errorWithTitle(event.getHook(), "Output Too Large", "The formatted result is too large to send. Please provide a smaller code snippet or use a paste service instead." | ||
| 48 | + Responses.errorWithTitle(event.getHook(), "Code out of Bounds", "The formatted result is too large to send. Please provide a smaller code snippet or use a paste service instead." | ||
| 48 | 49 | ).queue(); | |
| 49 | 50 | return; | |
| 50 | 51 | } | |
| 51 | 52 | ||
| 52 | - Responses.success(event.getHook(), "Success", "The formatted message is being sent to this channel.") | ||
| 53 | - .queue(success -> sendChunksInOrder(channel, messages, 0, target,event)); | ||
| 53 | + sendChunksInOrder(channel, messages, 0, target, event, 0L); | ||
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | 56 | ||
| 57 | - private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event) { | ||
| 57 | + private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event, long firstMessageID) { | ||
| 58 | 58 | if (index >= messages.size()) { | |
| 59 | + event.getHook().deleteOriginal().queue(_ -> | ||
| 60 | + event.getHook().sendMessage("Your message has been sent. If needed, you can change the language used for syntax highlighting below.") | ||
| 61 | + .setEphemeral(true) | ||
| 62 | + .setComponents(FormatCodeInteractionHandler.buildLanguageMenu(event.getUser().getIdLong(), messages.size(), firstMessageID)) | ||
| 63 | + .queue() | ||
| 64 | + ); | ||
| 59 | 65 | return; | |
| 60 | 66 | } | |
| 61 | - var action = channel.sendMessage(messages.get(index)) | ||
| 67 | + MessageCreateAction action = channel.sendMessage(messages.get(index)) | ||
| 62 | 68 | .setAllowedMentions(List.of()); | |
| 63 | 69 | ||
| 64 | 70 | if (index == messages.size() - 1) { | |
| 65 | - if(index == 0){ | ||
| 66 | - action.setComponents(buildActionRow(target, event.getUser().getIdLong())); | ||
| 71 | + if(messages.size() >1) { | ||
| 72 | + action.setComponents(buildMultiMessageActionRow(target, event.getUser().getIdLong(), messages.size(), firstMessageID)); | ||
| 67 | 73 | } else { | |
| 68 | - action.setComponents(buildActionRow(target)); | ||
| 74 | + action.setComponents(buildActionRow(target,event.getUser().getIdLong())); | ||
| 69 | 75 | } | |
| 70 | 76 | } | |
| 71 | 77 | ||
| 72 | - action.queue(success -> | ||
| 73 | - sendChunksInOrder(channel, messages, index + 1, target, event)); | ||
| 78 | + action.queue(sent -> sendChunksInOrder(channel, messages, index + 1, target, event, index == 0 ? sent.getIdLong() : firstMessageID)); | ||
| 74 | 79 | } | |
| 75 | 80 | ||
| 76 | - /** | ||
| 77 | - * Builds the action row placed on the last code-block message. | ||
| 78 | - * | ||
| 79 | - * @param target the original message linked by the "View Original" button | ||
| 80 | - * @return an action row containing the "View Original" link button | ||
| 81 | - */ | ||
| 82 | - @Contract("_ -> new") | ||
| 83 | - static @NotNull ActionRow buildActionRow(@NotNull Message target) { | ||
| 84 | - return ActionRow.of(Button.link(target.getJumpUrl(), "View Original")); | ||
| 81 | + @Contract("_,_,_,_ -> new") | ||
| 82 | + static @NotNull ActionRow buildMultiMessageActionRow(@NotNull Message target, long requesterId, int total, long firstMessageID) { | ||
| 83 | + return ActionRow.of( | ||
| 84 | + FormatCodeInteractionHandler.createDeleteAllButton(requesterId, total, firstMessageID ), | ||
| 85 | + Button.link(target.getJumpUrl(), "View Original")); | ||
| 85 | 86 | } | |
| 86 | 87 | ||
| 87 | - /** | ||
| 88 | - * Builds the action row placed on the file-upload message: a delete button and a "View Original" link. | ||
| 89 | - * | ||
| 90 | - * @param target the original message linked by the "View Original" button | ||
| 91 | - * @param requesterId the id of the user permitted to delete the message | ||
| 92 | - * @return an action row containing the delete and "View Original" buttons | ||
| 93 | - */ | ||
| 94 | - @Contract("_,_ -> new") | ||
| 88 | + @Contract("_,_-> new") | ||
| 95 | 89 | static @NotNull ActionRow buildActionRow(@NotNull Message target, long requesterId) { | |
| 96 | - return ActionRow.of(InteractionUtils.createDeleteButton(requesterId), | ||
| 90 | + return ActionRow.of( | ||
| 91 | + InteractionUtils.createDeleteButton(requesterId), | ||
| 97 | 92 | Button.link(target.getJumpUrl(), "View Original")); | |
| 98 | 93 | } | |
| 99 | 94 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,140 @@ | |||
| 1 | + package net.discordjug.javabot.systems.user_commands.format_code; | ||
| 2 | + | ||
| 3 | + import lombok.RequiredArgsConstructor; | ||
| 4 | + import net.discordjug.javabot.annotations.AutoDetectableComponentHandler; | ||
| 5 | + import net.discordjug.javabot.data.config.BotConfig; | ||
| 6 | + import net.discordjug.javabot.util.Checks; | ||
| 7 | + import net.discordjug.javabot.util.Responses; | ||
| 8 | + import net.dv8tion.jda.api.components.actionrow.ActionRow; | ||
| 9 | + import net.dv8tion.jda.api.components.buttons.Button; | ||
| 10 | + import net.dv8tion.jda.api.components.selections.SelectOption; | ||
| 11 | + import net.dv8tion.jda.api.components.selections.StringSelectMenu; | ||
| 12 | + import net.dv8tion.jda.api.entities.Member; | ||
| 13 | + import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; | ||
| 14 | + import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; | ||
| 15 | + import net.dv8tion.jda.api.interactions.components.ComponentInteraction; | ||
| 16 | + import org.jspecify.annotations.NonNull; | ||
| 17 | + import xyz.dynxsty.dih4jda.interactions.components.ButtonHandler; | ||
| 18 | + import xyz.dynxsty.dih4jda.interactions.components.StringSelectMenuHandler; | ||
| 19 | + import xyz.dynxsty.dih4jda.util.ComponentIdBuilder; | ||
| 20 | + | ||
| 21 | + import java.util.Arrays; | ||
| 22 | + import java.util.List; | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * Handles the interactive components on formatted code blocks: the delete-all button and the | ||
| 26 | + * language-selection dropdown. Both act on every message of a (possibly multi-message) block, | ||
| 27 | + * which is resolved via {@link LinkedMessages}. | ||
| 28 | + */ | ||
| 29 | + @AutoDetectableComponentHandler(FormatCodeInteractionHandler.COMPONENT_ID) | ||
| 30 | + @RequiredArgsConstructor | ||
| 31 | + public class FormatCodeInteractionHandler implements ButtonHandler, StringSelectMenuHandler { | ||
| 32 | + static final String COMPONENT_ID = "format-code"; | ||
| 33 | + private final BotConfig botConfig; | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * Builds the delete-all button placed on the last message of a code block. | ||
| 37 | + * | ||
| 38 | + * @param requesterID the id of the user allowed to delete the block | ||
| 39 | + * @param total the number of messages making up the block | ||
| 40 | + * @param firstMessageID the id of first message to check update code block | ||
| 41 | + * @return the delete-all button | ||
| 42 | + */ | ||
| 43 | + public static Button createDeleteAllButton(long requesterID, int total, long firstMessageID) { | ||
| 44 | + return Button.secondary(ComponentIdBuilder.build(COMPONENT_ID, requesterID, total,firstMessageID), "\uD83D\uDDD1\uFE0F"); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + @Override | ||
| 48 | + public void handleButton(ButtonInteractionEvent event, @NonNull Button button) { | ||
| 49 | + if (!isValid(event)) { | ||
| 50 | + return; | ||
| 51 | + } | ||
| 52 | + String[] id = ComponentIdBuilder.split(event.getComponentId()); | ||
| 53 | + | ||
| 54 | + event.deferEdit().queue(); | ||
| 55 | + | ||
| 56 | + LinkedMessages.resolveBefore(event.getMessage(), Integer.parseInt(id[2]), true, | ||
| 57 | + messages -> { | ||
| 58 | + if (messages.getLast().getIdLong() == Long.parseLong(id[3])) { | ||
| 59 | + event.getChannel().purgeMessages(messages); | ||
| 60 | + } else { | ||
| 61 | + Responses.error(event.getHook(), "The code block could not be deleted. The messages may have been deleted.").queue(); | ||
| 62 | + } | ||
| 63 | + }, () -> Responses.error(event.getHook(), "Could not delete the code block").queue()); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * Builds the language-selection dropdown row for a code block. | ||
| 68 | + * | ||
| 69 | + * @param requesterId the id of the user allowed to change the language | ||
| 70 | + * @param total the number of messages making up the block | ||
| 71 | + * @param firstMessageID the id of first message to check update code block | ||
| 72 | + * @return an action row containing the language dropdown | ||
| 73 | + */ | ||
| 74 | + public static ActionRow buildLanguageMenu(long requesterId, int total, long firstMessageID) { | ||
| 75 | + return ActionRow.of(languageMenu(ComponentIdBuilder.build(COMPONENT_ID, requesterId, total, firstMessageID))); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + @Override | ||
| 79 | + public void handleStringSelectMenu(@NonNull StringSelectInteractionEvent event, @NonNull List<String> values) { | ||
| 80 | + if (!isValid(event)) { | ||
| 81 | + return; | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + String[] id = ComponentIdBuilder.split(event.getComponentId()); | ||
| 85 | + Language language = Language.fromString(values.getFirst()); | ||
| 86 | + | ||
| 87 | + event.deferEdit().queue(); | ||
| 88 | + | ||
| 89 | + LinkedMessages.resolveBefore(event.getMessage(), Integer.parseInt(id[2]), false, | ||
| 90 | + messages -> { | ||
| 91 | + if (messages.getLast().getIdLong() == Long.parseLong(id[3])) { | ||
| 92 | + messages.forEach(message -> message.editMessage(withLanguage(message.getContentRaw(), language)).queue()); | ||
| 93 | + } else { | ||
| 94 | + Responses.error(event.getHook(), "The code block could not be updated. The messages may have been deleted.").queue(); | ||
| 95 | + } | ||
| 96 | + }, () -> Responses.error(event.getHook(), "Could not update the code block").queue()); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + private static StringSelectMenu languageMenu(String customId) { | ||
| 100 | + return StringSelectMenu.create(customId) | ||
| 101 | + .setPlaceholder("Change language") | ||
| 102 | + .addOptions(Arrays.stream(Language.values()) | ||
| 103 | + .filter(language -> language != Language.UNKNOWN) | ||
| 104 | + .map(language -> SelectOption.of(language.getDisplayName(), language.name())) | ||
| 105 | + .toList()) | ||
| 106 | + .build(); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + private boolean isValid(ComponentInteraction event) { | ||
| 110 | + String[] id = ComponentIdBuilder.split(event.getComponentId()); | ||
| 111 | + long requesterId = Long.parseLong(id[1]); | ||
| 112 | + | ||
| 113 | + Member member = event.getMember(); | ||
| 114 | + if (member == null) { | ||
| 115 | + Responses.errorWithTitle(event, "Server Required", "This may only be used inside a server.").queue(); | ||
| 116 | + return false; | ||
| 117 | + } | ||
| 118 | + if (member.getIdLong() != requesterId && !Checks.hasStaffRole(botConfig, member)) { | ||
| 119 | + Responses.errorWithTitle(event, "Access Denied", "You are not authorized to perform this action.").queue(); | ||
| 120 | + return false; | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + return true; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + /** | ||
| 127 | + * Re-wraps a code-block message in a different language by swapping the tag on its opening fence, | ||
| 128 | + * leaving the code itself untouched. | ||
| 129 | + * | ||
| 130 | + * @param content the raw message content, expected to start with a fenced code block | ||
| 131 | + * @param language the language to switch to | ||
| 132 | + * @return the message content with its opening fence set to {@code language} | ||
| 133 | + */ | ||
| 134 | + private static String withLanguage(String content, Language language) { | ||
| 135 | + int firstLineEnd = content.indexOf('\n'); | ||
| 136 | + return firstLineEnd < 0 | ||
| 137 | + ? content | ||
| 138 | + : "```" + language.getDiscordName() + content.substring(firstLineEnd); | ||
| 139 | + } | ||
| 140 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -27,6 +27,6 @@ public void execute(@NotNull MessageContextInteractionEvent event) { | |||
| 27 | 27 | ||
| 28 | 28 | Code code = new Code(Language.JAVA, content); | |
| 29 | 29 | ||
| 30 | - event.deferReply().queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget())); | ||
| 30 | + event.deferReply(true).queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget())); | ||
| 31 | 31 | } | |
| 32 | 32 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,61 @@ | |||
| 1 | + package net.discordjug.javabot.systems.user_commands.format_code; | ||
| 2 | + | ||
| 3 | + import net.dv8tion.jda.api.entities.Message; | ||
| 4 | + | ||
| 5 | + import java.util.ArrayList; | ||
| 6 | + import java.util.List; | ||
| 7 | + import java.util.function.Consumer; | ||
| 8 | + | ||
| 9 | + | ||
| 10 | + /** | ||
| 11 | + * Helper for acting on a block of related messages sent as a group — such as a piece of code split | ||
| 12 | + * across several Discord messages. Given one message of the block and the total count, it resolves | ||
| 13 | + * the whole block (only the bot's own messages) so a single interaction can delete or edit it. | ||
| 14 | + */ | ||
| 15 | + public class LinkedMessages { | ||
| 16 | + private LinkedMessages() { | ||
| 17 | + } | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * Resolves a block of messages ending at {@code triggerMessage} and passes the | ||
| 21 | + * bot's own messages to {@code onResolved}, ordered from newest to oldest. | ||
| 22 | + * If {@code inclusive} is {@code true}, {@code triggerMessage} is included in | ||
| 23 | + * the resolved block; otherwise, only the preceding {@code total} messages are | ||
| 24 | + * considered. When {@code inclusive} is {@code true}, {@code total} must be at | ||
| 25 | + * least 2: {@code triggerMessage} counts as one of the {@code total} messages | ||
| 26 | + * and the remaining {@code total - 1} are fetched from the channel history, so | ||
| 27 | + * a single-message inclusive block is not supported. Runs {@code onError} if | ||
| 28 | + * the block can't be safely resolved. | ||
| 29 | + * | ||
| 30 | + * @param triggerMessage the message marking the end of the block | ||
| 31 | + * @param total the number of messages to resolve; must be at least 2 when {@code inclusive} is {@code true} | ||
| 32 | + * @param inclusive whether {@code triggerMessage} should be included in the resolved block | ||
| 33 | + * @param onResolved receives the bot's messages, ordered from newest to oldest | ||
| 34 | + * @param onError runs if the block can't be safely resolved | ||
| 35 | + */ | ||
| 36 | + static void resolveBefore(Message triggerMessage, int total, boolean inclusive,Consumer<List<Message>> onResolved, Runnable onError) { | ||
| 37 | + triggerMessage.getChannel().getHistoryBefore(triggerMessage.getIdLong(), inclusive?total-1 :total).queue(history -> { | ||
| 38 | + List<Message> block = new ArrayList<>(history.getRetrievedHistory()); | ||
| 39 | + if (inclusive){ | ||
| 40 | + block.addFirst(triggerMessage); | ||
| 41 | + } | ||
| 42 | + verify(block, total, onResolved, onError); | ||
| 43 | + }); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + private static void verify(List<Message> messages, int total, Consumer<List<Message>> onResolved, Runnable onError) { | ||
| 47 | + List<Message> own = onlyOwn(messages); | ||
| 48 | + boolean allCodeBlocks = own.stream().allMatch(message -> message.getContentRaw().startsWith("```")); | ||
| 49 | + if (own.size() != total || !allCodeBlocks) { | ||
| 50 | + onError.run(); | ||
| 51 | + return; | ||
| 52 | + } | ||
| 53 | + onResolved.accept(own); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + private static List<Message> onlyOwn(List<Message> messages) { | ||
| 57 | + return messages.stream() | ||
| 58 | + .filter(message -> message.getAuthor().getIdLong() == message.getJDA().getSelfUser().getIdLong()) | ||
| 59 | + .toList(); | ||
| 60 | + } | ||
| 61 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -116,7 +116,7 @@ private boolean canDeleteUsingButton(Member member, String[] componentId) { | |||
| 116 | 116 | } | |
| 117 | 117 | ||
| 118 | 118 | public static Button createDeleteButton(long senderId) { | |
| 119 | - return Button.secondary(createDeleteInteractionId(senderId), "\uD83D\uDDD1️"); | ||
| 119 | + return Button.secondary(createDeleteInteractionId(senderId), "\uD83D\uDDD1\uFE0F"); | ||
| 120 | 120 | } | |
| 121 | 121 | ||
| 122 | 122 | public static String createDeleteInteractionId(long senderId) { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments