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
@@ -0,0 +1,78 @@
package net.discordjug.javabot.systems.user_commands.format_code;

import java.util.ArrayList;
import java.util.List;

/**
* Holds a piece of code and its {@link Language}, and turns it into
* Discord-friendly representations that respect Discord's 2000-character limit.
*/
public class Code {

/**
* Maximum characters per chunk. Discord's hard limit per message is 2000;
* the remaining headroom covers the surrounding ```language fences.
*/
private static final int MAX_SIZE = 1980;

private final Language language;
private final String content;

/**
* Creates a code block for the given language and content.
*
* @param language the language the code is written in, used for syntax highlighting
* @param content the raw, already-sanitized code to format
*/
public Code(Language language, String content) {
this.language = language;
this.content = content;
}

public String getContent() {

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 getContent() and setLanguage are unused. If so, please remove them.

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.

getContent() is still present.

@Neil-Tomar Neil-Tomar Jun 24, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

getContent() in now being used in FormatCodeDispatcher at line 34 and 44.

return content;
}

public Language getLanguage() {
return language;
}

/**
* Splits {@link #content} into pieces that each fit within {@link #MAX_SIZE},
* breaking on newlines where possible so lines are not cut in half.
*
* @return the content split into chunks that each fit within the limit
*/
private List<String> toDiscordChunks() {
List<String> chunks = new ArrayList<>();
String remaining = content;

while (remaining.length() > MAX_SIZE) {
int split = remaining.lastIndexOf('\n', MAX_SIZE);
if (split <= 0) {
// No newline in range (or only at the very start) -> hard cut,
// guaranteeing progress so this can never infinite-loop.
chunks.add(remaining.substring(0, MAX_SIZE));
remaining = remaining.substring(MAX_SIZE);
} else {
chunks.add(remaining.substring(0, split));
remaining = remaining.substring(split + 1); // +1 consumes the '\n'
}
}
chunks.add(remaining);
return chunks;
}

/**
* Splits the content into chunks that each fit within Discord's character limit and wraps
* every chunk in a language-tagged code block.
*
* @return the formatted code-block messages, one per Discord message
*/
public List<String> toDiscordMessages() {
return toDiscordChunks()
.stream()
.map(chunk -> String.format("```%s\n%s\n```", language.getDiscordName(), chunk))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package net.discordjug.javabot.systems.user_commands.format_code;


import net.discordjug.javabot.util.IndentationHelper;
import net.discordjug.javabot.util.StringUtils;
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionContextType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;

import org.jetbrains.annotations.NotNull;
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;

import java.util.List;

/**
* <h3>This class represents the "Format and Indent Code" Message Context command.</h3>
Expand All @@ -27,9 +24,12 @@ public FormatAndIndentCodeMessageContext() {

@Override
public void execute(@NotNull MessageContextInteractionEvent event) {
event.replyFormat("```java\n%s\n```", IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()), IndentationHelper.IndentationType.TABS))
.setAllowedMentions(List.of())
.setComponents(FormatCodeCommand.buildActionRow(event.getTarget(), event.getUser().getIdLong()))

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 action row including the buttons for deleting and the URL for jumping back are no longer present because you removed them here. Please add them to all messages.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

There is a reason for that. The buttons only affect the message they are attached to, so adding them to each message would only allow deletion of that specific message.

Adding buttons to every message would also introduce a visual break inside the code block, which could hurt readability, as shown in the screenshots.

If you'd prefer the buttons to appear only on the last message while still affecting the entire code block, I can look into implementing that. I'm not familiar with that approach yet, but I'm happy to investigate it.

image

Here is with buttons:

image

as you can see it's hard to read through indentation.

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.

If it is a single message, both buttons should definitely be there.
If it consists of multiple messages, at least the View Original button should be present in the last message. There could also be a delete button that deletes all codeblock messages on the last message (and the file upload message should also have both buttons).

.queue();
String indented = IndentationHelper.formatIndentation(
StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()),
IndentationHelper.IndentationType.TABS);

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

FormatCodeDispatcher.sendCode(code, event, event.getTarget());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@

import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
import net.discordjug.javabot.util.*;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.buttons.Button;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionContextType;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.jetbrains.annotations.Contract;

import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.List;

/**
* <h3>This class represents the /format-code command.</h3>
Expand All @@ -29,25 +26,7 @@ public FormatCodeCommand() {
.setContexts(InteractionContextType.GUILD)
.addOptions(
new OptionData(OptionType.STRING, "message-id", "Message to be formatted, last message used if left blank.", false),
new OptionData(OptionType.STRING, "format", "The language used to format the code, defaults to Java if left blank.", false)
.addChoice("C", "c")
.addChoice("C#", "csharp")
.addChoice("C++", "cpp")
.addChoice("CSS", "css")
.addChoice("D", "d")
.addChoice("Go", "go")
.addChoice("HTML", "html")
.addChoice("Java", "java")
.addChoice("JavaScript", "js")
.addChoice("Kotlin", "kotlin")
.addChoice("PHP", "php")
.addChoice("Python", "python")
.addChoice("Ruby", "ruby")
.addChoice("Rust", "rust")
.addChoice("SQL", "sql")
.addChoice("Swift", "swift")
.addChoice("TypeScript", "typescript")
.addChoice("XML", "xml"),
formatOption(),
new OptionData(OptionType.STRING,"auto-indent","The type of indentation applied to the message, does not automatically indent if left blank.",false)
.addChoice("Four Spaces","FOUR_SPACES")
.addChoice("Two Spaces","TWO_SPACES")
Expand All @@ -56,18 +35,28 @@ public FormatCodeCommand() {
);
}

@Contract("_ -> new")
static @NotNull ActionRow buildActionRow(@NotNull Message target, long requesterId) {
return ActionRow.of(InteractionUtils.createDeleteButton(requesterId),
Button.link(target.getJumpUrl(), "View Original"));
/**
* Builds the {@code format} option, generating one choice per {@link Language} (excluding
* {@link Language#UNKNOWN}) so the enum stays the single source of truth for the language list.
*
* @return the configured {@code format} option
*/
private static OptionData formatOption() {
OptionData option = new OptionData(OptionType.STRING, "format", "The language used to format the code, defaults to Java if left blank.", false);
for (Language language : Language.values()) {
if (language != Language.UNKNOWN) { // UNKNOWN is the fallback, not a real choice
option.addChoice(language.getDisplayName(), language.name()); // value = enum name so valueOf() reverses it
}
}
return option;
}

@Override
public void execute(@NotNull SlashCommandInteractionEvent event) {
OptionMapping idOption = event.getOption("message-id");
String format = event.getOption("format", "java", OptionMapping::getAsString);
Language language = event.getOption("format", Language.JAVA, o -> Language.fromString(o.getAsString()));
String indentation = event.getOption("auto-indent","NULL",OptionMapping::getAsString);
event.deferReply().queue();

if (idOption == null) {
event.getChannel().getHistory()
.retrievePast(10)
Expand All @@ -77,26 +66,32 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
.filter(m -> !m.getAuthor().isBot()).findFirst()
.orElse(null);
if (target != null) {
event.getHook().sendMessageFormat("```%s\n%s\n```", format, IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(target.getContentRaw()),IndentationHelper.IndentationType.valueOf(indentation)))
.setAllowedMentions(List.of())
.setComponents(buildActionRow(target, event.getUser().getIdLong()))
.queue();
sendFormattedCode(event, target, language, indentation);
} else {
Responses.error(event.getHook(), "Could not find message; please specify a message id.").queue();
Responses.errorWithTitle(event, "Message Not Found", "No recent user message could be found. Please specify a message ID.")
.queue();
}
});
} else {
if (Checks.isInvalidLongInput(idOption)) {
Responses.error(event.getHook(), "Please provide a valid message id!").queue();
Responses.errorWithTitle(event, "Invalid Message ID", "Please provide a valid Discord message ID.")
.queue();
return;
}
long messageId = idOption.getAsLong();
event.getChannel().retrieveMessageById(messageId).queue(
target -> event.getHook().sendMessageFormat("```%s\n%s\n```", format, IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(target.getContentRaw()), IndentationHelper.IndentationType.valueOf(indentation)))
.setAllowedMentions(List.of())
.setComponents(buildActionRow(target, event.getUser().getIdLong()))
.queue(),
e -> Responses.error(event.getHook(), "Could not retrieve message with id: " + messageId).queue());
target -> sendFormattedCode(event, target, language, indentation),
error -> Responses.errorWithTitle(event, "Message Not Found", "Could not retrieve the message with ID `" + messageId + "`. Make sure the message exists and is accessible.").queue());
}
}

private void sendFormattedCode(SlashCommandInteractionEvent event, Message target, Language language, String indentation) {
String content = IndentationHelper.formatIndentation(
StringUtils.standardSanitizer().compute(target.getContentRaw()),
IndentationHelper.IndentationType.valueOf(indentation));

Code code = new Code(language,content);

FormatCodeDispatcher.sendCode(code, event, target);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package net.discordjug.javabot.systems.user_commands.format_code;

import net.discordjug.javabot.util.*;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.buttons.Button;
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.utils.FileUpload;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nonnull;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
* Shared sending logic for the code-formatting commands. Replies with the full code as a
* downloadable file, then posts it as one or more ordered code-block messages that each respect
* Discord's 2000-character limit.
*/
class FormatCodeDispatcher {

/**
* The maximum number of code-block messages to post inline; longer code is sent only as a file.

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 update the Javadoc to say it shows an error in that situation.

*/
private static final int MAX_MESSAGES = 5;

/**
* Acknowledges the interaction by replying with the full code as a file, then posts the code as
* ordered code-block messages. Replies with an error instead if there is nothing to format.
*
* @param code the code to send
* @param event the interaction to reply to
* @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){
if (code.getContent().isBlank()) {
Responses.errorWithTitle(event, "404 Code not found","There is no code to format in that message.").queue();
return;
}

List<String> messages = code.toDiscordMessages();

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 show an error if messages.size() > 5 instead of sending the code.


// The reply both acknowledges the interaction and hands users the full,
// un-split code as a downloadable file (so chunking never loses anything).
FileUpload file = FileUpload.fromData(
code.getContent().getBytes(StandardCharsets.UTF_8),
"code." + code.getLanguage().getDiscordName()
);

MessageChannel channel = target.getChannel();

event.replyFiles(file)

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.

What is the reason to reply with a file vs just using event.reply... on the first message and skipping the file?

At least if it's only a single chunk, there shouldn't be any file (but I think it's also better without a file with multiple messages).

Make sure to update the Javadoc accordingly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I tried dropping the file and replying with the first chunk as suggested, but it hits two problems:

Sending the chunks as plain messages with no reply leaves the interaction unacknowledged → "This application did not respond."
Replying with the first chunk does acknowledge it, but the reply renders as its own attributed block, separate from the follow-up messages, so the code gets split visually mid-block.
image

Replying with the file avoids both: it acknowledges the interaction, and since the reply is a file (not a chunk) the code-block messages stay uniform with no break — plus it's a clean, copyable full version that reads nicely on desktop. The file seemed like the most useful thing to put there.

So, my preference would actually be to keep the file for both the single- and multi-chunk cases — it gives a consistent experience either way. But if you'd rather it was gone, I'm happy to remove it for the single-chunk case (just reply with the one block directly), or if you have a different approach in mind, I'll gladly go with that.

@danthe1st danthe1st Jun 24, 2026

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.

In that case, please replace it with an ephemeral response:

Responses.success(event, "Success", "The formatted message is being sent to this channel.").queue();

(or similar)

There is no reason to clutter the public channel with the file.

.setAllowedMentions(List.of())
.setComponents(buildActionRow(target, event.getUser().getIdLong()))
.queue(success -> sendChunksInOrder(channel, messages, 0, target,event));
}


private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event) {
if (index >= messages.size()) {
return;
}
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."
).queue();
return;
}
var action = channel.sendMessage(messages.get(index))
Comment thread
danthe1st marked this conversation as resolved.
.setAllowedMentions(List.of());

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

action.queue(success ->
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),
Button.link(target.getJumpUrl(), "View Original"));
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package net.discordjug.javabot.systems.user_commands.format_code;

import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
import net.discordjug.javabot.util.StringUtils;
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionContextType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;

import org.jetbrains.annotations.NotNull;

import java.util.List;

/**
* <h3>This class represents the "Format Code" Message Context command.</h3>
Expand All @@ -25,9 +23,10 @@ public FormatCodeMessageContext() {

@Override
public void execute(@NotNull MessageContextInteractionEvent event) {
event.replyFormat("```java\n%s\n```", StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()))
.setAllowedMentions(List.of())
.setComponents(FormatCodeCommand.buildActionRow(event.getTarget(), event.getUser().getIdLong()))
.queue();
String content = StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw());

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

FormatCodeDispatcher.sendCode(code, event, event.getTarget());
}
}
Loading