Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.CommandInteraction;
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

Expand Down Expand Up @@ -33,9 +34,9 @@ class FormatCodeDispatcher {
* @param target the original message the code came from, used for the channel and the
* "View Original" / delete buttons
*/
public static void sendCode(Code code, @Nonnull CommandInteraction event, Message target){
public static void sendCode(Code code, @Nonnull CommandInteraction event, Message target) {
if (code.getContent().isBlank()) {
Responses.errorWithTitle(event.getHook(), "404 Code not found","There is no code to format in that message.").queue();
Responses.errorWithTitle(event.getHook(), "404 Code not found", "There is no code to format in that message.").queue();
return;
}

Expand All @@ -44,56 +45,50 @@ public static void sendCode(Code code, @Nonnull CommandInteraction event, Messag
MessageChannel channel = target.getChannel();

if (messages.size() > MAX_MESSAGES) {
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."
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."
).queue();
return;
}

Responses.success(event.getHook(), "Success", "The formatted message is being sent to this channel.")
.queue(success -> sendChunksInOrder(channel, messages, 0, target,event));
sendChunksInOrder(channel, messages, 0, target, event, 0L);
}


private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event) {
private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event, long firstMessageID) {
if (index >= messages.size()) {
event.getHook().deleteOriginal().queue(_ ->
event.getHook().sendMessage("Your message has been sent. If needed, you can change the language used for syntax highlighting below.")
.setEphemeral(true)
.setComponents(FormatCodeInteractionHandler.buildLanguageMenu(event.getUser().getIdLong(), messages.size(), firstMessageID))
.queue()
);
return;
}
var action = channel.sendMessage(messages.get(index))
MessageCreateAction action = channel.sendMessage(messages.get(index))
.setAllowedMentions(List.of());

if (index == messages.size() - 1) {
if(index == 0){
action.setComponents(buildActionRow(target, event.getUser().getIdLong()));
if(messages.size() >1) {
action.setComponents(buildMultiMessageActionRow(target, event.getUser().getIdLong(), messages.size(), firstMessageID));
} else {
action.setComponents(buildActionRow(target));
action.setComponents(buildActionRow(target,event.getUser().getIdLong()));
}
}

action.queue(success ->
sendChunksInOrder(channel, messages, index + 1, target, event));
action.queue(sent -> sendChunksInOrder(channel, messages, index + 1, target, event, index == 0 ? sent.getIdLong() : firstMessageID));
}

/**
* 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"));
@Contract("_,_,_,_ -> new")
static @NotNull ActionRow buildMultiMessageActionRow(@NotNull Message target, long requesterId, int total, long firstMessageID) {
return ActionRow.of(
FormatCodeInteractionHandler.createDeleteAllButton(requesterId, total, firstMessageID ),
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")
@Contract("_,_-> new")
static @NotNull ActionRow buildActionRow(@NotNull Message target, long requesterId) {
return ActionRow.of(InteractionUtils.createDeleteButton(requesterId),
return ActionRow.of(
InteractionUtils.createDeleteButton(requesterId),
Button.link(target.getJumpUrl(), "View Original"));
}
}
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");
}

Comment thread
danthe1st marked this conversation as resolved.
@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 button may only be used inside a server.").queue();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I think isValid is also used for the menu which isn't a button, I think This may only be used inside a server. is better.

return false;
}
if (!(member.getIdLong() == requesterId || Checks.hasStaffRole(botConfig, member))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please change this to if (member.getIdLong() != requesterId || !Checks.hasStaffRole(botConfig, member))? (basically just De Morgan) While it is doing the same thing, I think it is easier to read.

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ public void execute(@NotNull MessageContextInteractionEvent event) {

Code code = new Code(Language.JAVA, content);

event.deferReply().queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget()));
event.deferReply(true).queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget()));
}
}
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. 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
* @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) {
if (total <= 1 && inclusive) {
verify(List.of(triggerMessage), total, onResolved, onError);
return;
}
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private boolean canDeleteUsingButton(Member member, String[] componentId) {
}

public static Button createDeleteButton(long senderId) {
return Button.secondary(createDeleteInteractionId(senderId), "\uD83D\uDDD1");
return Button.secondary(createDeleteInteractionId(senderId), "\uD83D\uDDD1\uFE0F");
}

public static String createDeleteInteractionId(long senderId) {
Expand Down