-
Notifications
You must be signed in to change notification settings - Fork 25
feat: delete-all button and language dropdown for multi-message code blocks #550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
danthe1st
merged 7 commits into
Java-Discord:main
from
Neil-Tomar:feat/format-code-message-linking
Jul 11, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
c42529c
feat: link multi-message code blocks for delete-all and language swit…
Neil-Tomar 50e3fdf
refactor: address review feedback
Neil-Tomar 87a9552
fixed small mistakes.
Neil-Tomar 8dece97
fixed issues with approach 2
Neil-Tomar 3bc9739
fixed minor issues
Neil-Tomar e201e4a
fixed minor issues.
Neil-Tomar 16fb9d0
fix logical operation in permission check
danthe1st File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
...et/discordjug/javabot/systems/user_commands/format_code/FormatCodeInteractionHandler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| 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.events.interaction.component.ButtonInteractionEvent; | ||
| import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; | ||
| import net.dv8tion.jda.api.interactions.components.ComponentInteraction; | ||
| 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 | ||
| * @param firstMessageID the id of first message to check update code block | ||
| * @return the delete-all button | ||
| */ | ||
| public static Button createDeleteAllButton(long requesterID, int total, long firstMessageID) { | ||
| return Button.secondary(ComponentIdBuilder.build(COMPONENT_ID, requesterID, total,firstMessageID), "\uD83D\uDDD1\uFE0F"); | ||
| } | ||
|
|
||
| @Override | ||
| public void handleButton(ButtonInteractionEvent event, @NonNull Button button) { | ||
| if (!isValid(event)) { | ||
| return; | ||
| } | ||
| String[] id = ComponentIdBuilder.split(event.getComponentId()); | ||
|
|
||
| event.deferEdit().queue(); | ||
|
|
||
| LinkedMessages.resolveBefore(event.getMessage(), Integer.parseInt(id[2]), true, | ||
| messages -> { | ||
| if (messages.getLast().getIdLong() == Long.parseLong(id[3])) { | ||
| event.getChannel().purgeMessages(messages); | ||
| } else { | ||
| Responses.error(event.getHook(), "The code block could not be deleted. The messages may have been deleted.").queue(); | ||
| } | ||
| }, () -> Responses.error(event.getHook(), "Could not delete the code block").queue()); | ||
| } | ||
|
|
||
| /** | ||
| * 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 | ||
| * @param firstMessageID the id of first message to check update code block | ||
| * @return an action row containing the language dropdown | ||
| */ | ||
| public static ActionRow buildLanguageMenu(long requesterId, int total, long firstMessageID) { | ||
| return ActionRow.of(languageMenu(ComponentIdBuilder.build(COMPONENT_ID, requesterId, total, firstMessageID))); | ||
| } | ||
|
|
||
| @Override | ||
| public void handleStringSelectMenu(@NonNull StringSelectInteractionEvent event, @NonNull List<String> values) { | ||
| if (!isValid(event)) { | ||
| return; | ||
| } | ||
|
|
||
| String[] id = ComponentIdBuilder.split(event.getComponentId()); | ||
| Language language = Language.fromString(values.getFirst()); | ||
|
|
||
| event.deferEdit().queue(); | ||
|
|
||
| LinkedMessages.resolveBefore(event.getMessage(), Integer.parseInt(id[2]), false, | ||
| messages -> { | ||
| if (messages.getLast().getIdLong() == Long.parseLong(id[3])) { | ||
| messages.forEach(message -> message.editMessage(withLanguage(message.getContentRaw(), language)).queue()); | ||
| } else { | ||
| Responses.error(event.getHook(), "The code block could not be updated. The messages may have been deleted.").queue(); | ||
| } | ||
| }, () -> Responses.error(event.getHook(), "Could not update the code block").queue()); | ||
| } | ||
|
|
||
| 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(); | ||
| } | ||
|
|
||
| private boolean isValid(ComponentInteraction event) { | ||
| String[] id = ComponentIdBuilder.split(event.getComponentId()); | ||
| long requesterId = Long.parseLong(id[1]); | ||
|
|
||
| Member member = event.getMember(); | ||
| if (member == null) { | ||
| Responses.errorWithTitle(event, "Server Required", "This may only be used inside a server.").queue(); | ||
| return false; | ||
| } | ||
| if (member.getIdLong() != requesterId && !Checks.hasStaffRole(botConfig, member)) { | ||
| Responses.errorWithTitle(event, "Access Denied", "You are not authorized to perform this action.").queue(); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Re-wraps a code-block message in a different language by swapping the tag on its opening fence, | ||
| * leaving the code itself untouched. | ||
| * | ||
| * @param content the raw message content, expected to start with a fenced code block | ||
| * @param language the language to switch to | ||
| * @return the message content with its opening fence set to {@code language} | ||
| */ | ||
| private static String withLanguage(String content, Language language) { | ||
| int firstLineEnd = content.indexOf('\n'); | ||
| return firstLineEnd < 0 | ||
| ? content | ||
| : "```" + language.getDiscordName() + content.substring(firstLineEnd); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
src/main/java/net/discordjug/javabot/systems/user_commands/format_code/LinkedMessages.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package net.discordjug.javabot.systems.user_commands.format_code; | ||
|
|
||
| import net.dv8tion.jda.api.entities.Message; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.function.Consumer; | ||
|
|
||
|
|
||
| /** | ||
| * Helper for acting on a block of related messages sent as a group — such as a piece of code split | ||
| * across several Discord messages. Given one message of the block and the total count, it resolves | ||
| * the whole block (only the bot's own messages) so a single interaction can delete or edit it. | ||
| */ | ||
| public class LinkedMessages { | ||
| private LinkedMessages() { | ||
| } | ||
|
|
||
| /** | ||
| * Resolves a block of messages ending at {@code triggerMessage} and passes the | ||
| * bot's own messages to {@code onResolved}, ordered from newest to oldest. | ||
| * If {@code inclusive} is {@code true}, {@code triggerMessage} is included in | ||
| * the resolved block; otherwise, only the preceding {@code total} messages are | ||
| * considered. When {@code inclusive} is {@code true}, {@code total} must be at | ||
| * least 2: {@code triggerMessage} counts as one of the {@code total} messages | ||
| * and the remaining {@code total - 1} are fetched from the channel history, so | ||
| * a single-message inclusive block is not supported. Runs {@code onError} if | ||
| * the block can't be safely resolved. | ||
| * | ||
| * @param triggerMessage the message marking the end of the block | ||
| * @param total the number of messages to resolve; must be at least 2 when {@code inclusive} is {@code true} | ||
| * @param inclusive whether {@code triggerMessage} should be included in the resolved block | ||
| * @param onResolved receives the bot's messages, ordered from newest to oldest | ||
| * @param onError runs if the block can't be safely resolved | ||
| */ | ||
| static void resolveBefore(Message triggerMessage, int total, boolean inclusive,Consumer<List<Message>> onResolved, Runnable onError) { | ||
| triggerMessage.getChannel().getHistoryBefore(triggerMessage.getIdLong(), inclusive?total-1 :total).queue(history -> { | ||
| List<Message> block = new ArrayList<>(history.getRetrievedHistory()); | ||
| if (inclusive){ | ||
| block.addFirst(triggerMessage); | ||
| } | ||
| verify(block, total, onResolved, onError); | ||
| }); | ||
| } | ||
|
|
||
| private static void verify(List<Message> messages, int total, Consumer<List<Message>> onResolved, Runnable onError) { | ||
| List<Message> own = onlyOwn(messages); | ||
| boolean allCodeBlocks = own.stream().allMatch(message -> message.getContentRaw().startsWith("```")); | ||
| if (own.size() != total || !allCodeBlocks) { | ||
| onError.run(); | ||
| return; | ||
| } | ||
| onResolved.accept(own); | ||
| } | ||
|
|
||
| private static List<Message> onlyOwn(List<Message> messages) { | ||
| return messages.stream() | ||
| .filter(message -> message.getAuthor().getIdLong() == message.getJDA().getSelfUser().getIdLong()) | ||
| .toList(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.