Skip to content

Commit 9a03a81

Browse files
authored
Merge pull request #549 from Neil-Tomar/fix/format-code-long-messages
Fix formatting of code longer than Discord's message limit
2 parents b3970e6 + c6e158e commit 9a03a81

8 files changed

Lines changed: 375 additions & 72 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package net.discordjug.javabot.systems.user_commands.format_code;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
/**
7+
* Holds a piece of code and its {@link Language}, and turns it into
8+
* Discord-friendly representations that respect Discord's 2000-character limit.
9+
*/
10+
public class Code {
11+
12+
/**
13+
* Maximum characters per chunk. Discord's hard limit per message is 2000;
14+
* the remaining headroom covers the surrounding ```language fences.
15+
*/
16+
private static final int MAX_SIZE = 1980;
17+
18+
private final Language language;
19+
private final String content;
20+
21+
/**
22+
* Creates a code block for the given language and content.
23+
*
24+
* @param language the language the code is written in, used for syntax highlighting
25+
* @param content the raw, already-sanitized code to format
26+
*/
27+
public Code(Language language, String content) {
28+
this.language = language;
29+
this.content = content;
30+
}
31+
32+
public String getContent() {
33+
return content;
34+
}
35+
36+
/**
37+
* Splits {@link #content} into pieces that each fit within {@link #MAX_SIZE},
38+
* breaking on newlines where possible so lines are not cut in half.
39+
*
40+
* @return the content split into chunks that each fit within the limit
41+
*/
42+
private List<String> toDiscordChunks() {
43+
List<String> chunks = new ArrayList<>();
44+
String remaining = content;
45+
46+
while (remaining.length() > MAX_SIZE) {
47+
int split = remaining.lastIndexOf('\n', MAX_SIZE);
48+
if (split <= 0) {
49+
// No newline in range (or only at the very start) -> hard cut,
50+
// guaranteeing progress so this can never infinite-loop.
51+
chunks.add(remaining.substring(0, MAX_SIZE));
52+
remaining = remaining.substring(MAX_SIZE);
53+
} else {
54+
chunks.add(remaining.substring(0, split));
55+
remaining = remaining.substring(split + 1); // +1 consumes the '\n'
56+
}
57+
}
58+
chunks.add(remaining);
59+
return chunks;
60+
}
61+
62+
/**
63+
* Splits the content into chunks that each fit within Discord's character limit and wraps
64+
* every chunk in a language-tagged code block.
65+
*
66+
* @return the formatted code-block messages, one per Discord message
67+
*/
68+
public List<String> toDiscordMessages() {
69+
return toDiscordChunks()
70+
.stream()
71+
.map(chunk -> String.format("```%s\n%s\n```", language.getDiscordName(), chunk))
72+
.toList();
73+
}
74+
}

src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatAndIndentCodeMessageContext.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
package net.discordjug.javabot.systems.user_commands.format_code;
22

3-
43
import net.discordjug.javabot.util.IndentationHelper;
54
import net.discordjug.javabot.util.StringUtils;
65
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
76
import net.dv8tion.jda.api.interactions.InteractionContextType;
87
import net.dv8tion.jda.api.interactions.commands.build.Commands;
9-
108
import org.jetbrains.annotations.NotNull;
119
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
1210

13-
import java.util.List;
1411

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

2825
@Override
2926
public void execute(@NotNull MessageContextInteractionEvent event) {
30-
event.replyFormat("```java\n%s\n```", IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()), IndentationHelper.IndentationType.TABS))
31-
.setAllowedMentions(List.of())
32-
.setComponents(FormatCodeCommand.buildActionRow(event.getTarget(), event.getUser().getIdLong()))
33-
.queue();
27+
String indented = IndentationHelper.formatIndentation(
28+
StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()),
29+
IndentationHelper.IndentationType.TABS);
30+
31+
Code code = new Code(Language.JAVA, indented);
32+
33+
event.deferReply().queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget()));
3434
}
3535
}

src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeCommand.java

Lines changed: 50 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,15 @@
22

33
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
44
import net.discordjug.javabot.util.*;
5-
import net.dv8tion.jda.api.components.actionrow.ActionRow;
6-
import net.dv8tion.jda.api.components.buttons.Button;
75
import net.dv8tion.jda.api.entities.Message;
86
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
97
import net.dv8tion.jda.api.interactions.InteractionContextType;
108
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
119
import net.dv8tion.jda.api.interactions.commands.OptionType;
1210
import net.dv8tion.jda.api.interactions.commands.build.Commands;
1311
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
14-
import org.jetbrains.annotations.Contract;
15-
import org.jetbrains.annotations.NotNull;
1612

17-
import java.util.Collections;
18-
import java.util.List;
13+
import org.jetbrains.annotations.NotNull;
1914

2015
/**
2116
* <h3>This class represents the /format-code command.</h3>
@@ -29,25 +24,7 @@ public FormatCodeCommand() {
2924
.setContexts(InteractionContextType.GUILD)
3025
.addOptions(
3126
new OptionData(OptionType.STRING, "message-id", "Message to be formatted, last message used if left blank.", false),
32-
new OptionData(OptionType.STRING, "format", "The language used to format the code, defaults to Java if left blank.", false)
33-
.addChoice("C", "c")
34-
.addChoice("C#", "csharp")
35-
.addChoice("C++", "cpp")
36-
.addChoice("CSS", "css")
37-
.addChoice("D", "d")
38-
.addChoice("Go", "go")
39-
.addChoice("HTML", "html")
40-
.addChoice("Java", "java")
41-
.addChoice("JavaScript", "js")
42-
.addChoice("Kotlin", "kotlin")
43-
.addChoice("PHP", "php")
44-
.addChoice("Python", "python")
45-
.addChoice("Ruby", "ruby")
46-
.addChoice("Rust", "rust")
47-
.addChoice("SQL", "sql")
48-
.addChoice("Swift", "swift")
49-
.addChoice("TypeScript", "typescript")
50-
.addChoice("XML", "xml"),
27+
formatOption(),
5128
new OptionData(OptionType.STRING,"auto-indent","The type of indentation applied to the message, does not automatically indent if left blank.",false)
5229
.addChoice("Four Spaces","FOUR_SPACES")
5330
.addChoice("Two Spaces","TWO_SPACES")
@@ -56,47 +33,66 @@ public FormatCodeCommand() {
5633
);
5734
}
5835

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

6552
@Override
6653
public void execute(@NotNull SlashCommandInteractionEvent event) {
6754
OptionMapping idOption = event.getOption("message-id");
68-
String format = event.getOption("format", "java", OptionMapping::getAsString);
55+
Language language = event.getOption("format", Language.JAVA, o -> Language.fromString(o.getAsString()));
6956
String indentation = event.getOption("auto-indent","NULL",OptionMapping::getAsString);
70-
event.deferReply().queue();
57+
7158
if (idOption == null) {
72-
event.getChannel().getHistory()
73-
.retrievePast(10)
74-
.queue(messages -> {
75-
Collections.reverse(messages);
76-
Message target = messages.stream()
77-
.filter(m -> !m.getAuthor().isBot()).findFirst()
78-
.orElse(null);
79-
if (target != null) {
80-
event.getHook().sendMessageFormat("```%s\n%s\n```", format, IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(target.getContentRaw()),IndentationHelper.IndentationType.valueOf(indentation)))
81-
.setAllowedMentions(List.of())
82-
.setComponents(buildActionRow(target, event.getUser().getIdLong()))
83-
.queue();
84-
} else {
85-
Responses.error(event.getHook(), "Could not find message; please specify a message id.").queue();
86-
}
87-
});
59+
event.deferReply().queue(_ -> {
60+
event.getChannel().getHistory()
61+
.retrievePast(10)
62+
.queue(messages -> {
63+
Message target = messages.stream()
64+
.filter(m -> !m.getAuthor().isBot()).findFirst()
65+
.orElse(null);
66+
if (target != null) {
67+
sendFormattedCode(event, target, language, indentation);
68+
} else {
69+
Responses.errorWithTitle(event.getHook(), "Message Not Found", "No recent user message could be found. Please specify a message ID.")
70+
.queue();
71+
}
72+
});
73+
});
8874
} else {
8975
if (Checks.isInvalidLongInput(idOption)) {
90-
Responses.error(event.getHook(), "Please provide a valid message id!").queue();
76+
Responses.errorWithTitle(event, "Invalid Message ID", "Please provide a valid Discord message ID.")
77+
.queue();
9178
return;
9279
}
9380
long messageId = idOption.getAsLong();
94-
event.getChannel().retrieveMessageById(messageId).queue(
95-
target -> event.getHook().sendMessageFormat("```%s\n%s\n```", format, IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(target.getContentRaw()), IndentationHelper.IndentationType.valueOf(indentation)))
96-
.setAllowedMentions(List.of())
97-
.setComponents(buildActionRow(target, event.getUser().getIdLong()))
98-
.queue(),
99-
e -> Responses.error(event.getHook(), "Could not retrieve message with id: " + messageId).queue());
81+
event.deferReply().queue(_ -> {
82+
event.getChannel().retrieveMessageById(messageId).queue(
83+
target -> sendFormattedCode(event, target, language, indentation),
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());
85+
});
10086
}
10187
}
88+
89+
private void sendFormattedCode(SlashCommandInteractionEvent event, Message target, Language language, String indentation) {
90+
String content = IndentationHelper.formatIndentation(
91+
StringUtils.standardSanitizer().compute(target.getContentRaw()),
92+
IndentationHelper.IndentationType.valueOf(indentation));
93+
94+
Code code = new Code(language,content);
95+
96+
FormatCodeDispatcher.sendCode(code, event, target);
97+
}
10298
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package net.discordjug.javabot.systems.user_commands.format_code;
2+
3+
import net.discordjug.javabot.util.*;
4+
import net.dv8tion.jda.api.components.actionrow.ActionRow;
5+
import net.dv8tion.jda.api.components.buttons.Button;
6+
import net.dv8tion.jda.api.entities.Message;
7+
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
8+
import net.dv8tion.jda.api.interactions.commands.CommandInteraction;
9+
import org.jetbrains.annotations.Contract;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
import javax.annotation.Nonnull;
13+
import java.util.List;
14+
15+
/**
16+
* Shared sending logic for the code-formatting commands. Replies with the full code as a
17+
* downloadable file, then posts it as one or more ordered code-block messages that each respect
18+
* Discord's 2000-character limit.
19+
*/
20+
class FormatCodeDispatcher {
21+
22+
/**
23+
* The maximum number of code-block messages to post inline; longer code results in an error.
24+
*/
25+
private static final int MAX_MESSAGES = 5;
26+
27+
/**
28+
* Acknowledges the interaction by replying with the full code as a file, then posts the code as
29+
* ordered code-block messages. Replies with an error instead if there is nothing to format.
30+
*
31+
* @param code the code to send
32+
* @param event the interaction to reply to
33+
* @param target the original message the code came from, used for the channel and the
34+
* "View Original" / delete buttons
35+
*/
36+
public static void sendCode(Code code, @Nonnull CommandInteraction event, Message target){
37+
if (code.getContent().isBlank()) {
38+
Responses.errorWithTitle(event.getHook(), "404 Code not found","There is no code to format in that message.").queue();
39+
return;
40+
}
41+
42+
List<String> messages = code.toDiscordMessages();
43+
44+
MessageChannel channel = target.getChannel();
45+
46+
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+
).queue();
49+
return;
50+
}
51+
52+
Responses.success(event.getHook(), "Success", "The formatted message is being sent to this channel.")
53+
.queue(success -> sendChunksInOrder(channel, messages, 0, target,event));
54+
}
55+
56+
57+
private static void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, Message target, @Nonnull CommandInteraction event) {
58+
if (index >= messages.size()) {
59+
return;
60+
}
61+
var action = channel.sendMessage(messages.get(index))
62+
.setAllowedMentions(List.of());
63+
64+
if (index == messages.size() - 1) {
65+
if(index == 0){
66+
action.setComponents(buildActionRow(target, event.getUser().getIdLong()));
67+
} else {
68+
action.setComponents(buildActionRow(target));
69+
}
70+
}
71+
72+
action.queue(success ->
73+
sendChunksInOrder(channel, messages, index + 1, target, event));
74+
}
75+
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"));
85+
}
86+
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")
95+
static @NotNull ActionRow buildActionRow(@NotNull Message target, long requesterId) {
96+
return ActionRow.of(InteractionUtils.createDeleteButton(requesterId),
97+
Button.link(target.getJumpUrl(), "View Original"));
98+
}
99+
}

src/main/java/net/discordjug/javabot/systems/user_commands/format_code/FormatCodeMessageContext.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package net.discordjug.javabot.systems.user_commands.format_code;
22

3-
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
43
import net.discordjug.javabot.util.StringUtils;
4+
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
55
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
66
import net.dv8tion.jda.api.interactions.InteractionContextType;
77
import net.dv8tion.jda.api.interactions.commands.build.Commands;
8-
98
import org.jetbrains.annotations.NotNull;
109

11-
import java.util.List;
1210

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

2624
@Override
2725
public void execute(@NotNull MessageContextInteractionEvent event) {
28-
event.replyFormat("```java\n%s\n```", StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()))
29-
.setAllowedMentions(List.of())
30-
.setComponents(FormatCodeCommand.buildActionRow(event.getTarget(), event.getUser().getIdLong()))
31-
.queue();
26+
String content = StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw());
27+
28+
Code code = new Code(Language.JAVA, content);
29+
30+
event.deferReply().queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget()));
3231
}
3332
}

0 commit comments

Comments
 (0)