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 @@ -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));
}

Expand All @@ -62,38 +64,16 @@ private static void sendChunksInOrder(MessageChannel channel, List<String> 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"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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 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-delete";

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.

The select menu isn't about deleting hence the id format-code-delete isn't appropriate.

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️");

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.

I am not sure why such a special 1 character is used by InteractionUtils but it would be good if you could change it to use a normal 1 instead of it.

}

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, 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();
var channel = event.getChannel();

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 use the proper type here? I assume it's a MessageChannel?

LinkedMessages.resolve(channel, event.getMessage(), Integer.parseInt(id[2]),
messages -> messages.forEach(message -> message.delete().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.

}

@Override
public void handleStringSelectMenu(@NonNull StringSelectInteractionEvent event, @NonNull List<String> values) {

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.

The indentation is messed up here.

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();

LinkedMessages.resolveForward(event.getChannel(), event.getMessage(), Integer.parseInt(id[2]),

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.

Is the channel argument really needed here? Can't you derive it from the message?

messages -> messages.forEach(message ->
message.editMessage(withLanguage(message.getContentRaw(), language)).queue()));
}

private boolean canManage(Member member, long requesterId) {
return member.getIdLong() == requesterId
|| Checks.hasStaffRole(botConfig, member)
|| member.isOwner() ;
}

/**
* 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,60 @@
package net.discordjug.javabot.systems.user_commands.format_code;

import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;

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 the block ending at {@code triggerMessage} (walking back {@code total} messages) and
* passes the bot's own messages among them to {@code onResolved}.
*
* @param channel the channel the block is in
* @param triggerMessage the last message of the block (carries the component)
* @param total the number of messages in the block
* @param onResolved receives the bot's messages that make up the block
*/
static void resolve(MessageChannel channel, Message triggerMessage, int total, Consumer<List<Message>> onResolved) {

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.

I think you don't need the channel argument as you can use triggerMessage.getChannel() instead?

if (total <= 1) {
onResolved.accept(List.of(triggerMessage));
return;
}
channel.getHistoryBefore(triggerMessage.getIdLong(), total - 1).queue(history -> {
List<Message> block = new ArrayList<>(history.getRetrievedHistory());
block.add(triggerMessage);
onResolved.accept(onlyOwn(channel, block));

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.

Please fail (i.e. return an ephemeral error message) without deleting any messages in the following situations:

  • The message count after calling onlyOwn is different from the expected message count (total), Otherwise it would "forget" to delete messages in case there are user messages in between and I think it's better to fail properly rather than performing an incomplete deletion.
  • Any of the messages to delete don't start with a codeblock (as a safety mechanism)

You could do that by just adding an "error" Consumer.

});
}

/**
* Resolves the block of {@code total} messages sent after {@code anchorMessage} and passes the
* bot's own messages among them to {@code onResolved}.
*
* @param channel the channel the block is in
* @param anchorMessage the message just before the block (carries the component)
* @param total the number of messages in the block
* @param onResolved receives the bot's messages that make up the block
*/
static void resolveForward(MessageChannel channel, Message anchorMessage, int total, Consumer<List<Message>> onResolved) {

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.

I think you don't need the channel argument as you can use anchorMessage.getChannel() instead?

channel.getHistoryAfter(anchorMessage.getIdLong(), total).queue(history ->
onResolved.accept(onlyOwn(channel, history.getRetrievedHistory())));

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.

Please also perform the same message check/error handling approach here as well.

}

private static List<Message> onlyOwn(MessageChannel channel, List<Message> messages) {

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.

I think the channel argument isn't used here?

long selfId = channel.getJDA().getSelfUser().getIdLong();
return messages.stream()
.filter(message -> message.getAuthor().getIdLong() == selfId)
.toList();
}
}