Skip to content

Commit 7679c4b

Browse files
committed
fix formatting of code longer than Discord's message limit
1 parent b3970e6 commit 7679c4b

5 files changed

Lines changed: 258 additions & 19 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 Language language;
19+
private final String content;
20+
21+
public Code(Language language, String content) {
22+
this.language = language;
23+
this.content = content;
24+
}
25+
26+
public String getContent() {
27+
return content;
28+
}
29+
30+
public Language getLanguage() {
31+
return language;
32+
}
33+
34+
public void setLanguage(Language language) {
35+
this.language = language;
36+
}
37+
38+
/**
39+
* Splits {@link #content} into pieces that each fit within {@link #MAX_SIZE},
40+
* breaking on newlines where possible so lines are not cut in half.
41+
*/
42+
public 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+
/** Wraps each chunk in a language-tagged Discord code block. */
63+
public List<String> toDiscordMessages() {
64+
return toDiscordChunks()
65+
.stream()
66+
.map(chunk -> String.format("```%s\n%s\n```", language.getDiscordName(), chunk))
67+
.toList();
68+
}
69+
}

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

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package net.discordjug.javabot.systems.user_commands.format_code;
22

3-
3+
import net.discordjug.javabot.util.ExceptionLogger;
44
import net.discordjug.javabot.util.IndentationHelper;
5+
import net.discordjug.javabot.util.Responses;
56
import net.discordjug.javabot.util.StringUtils;
7+
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
68
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
79
import net.dv8tion.jda.api.interactions.InteractionContextType;
810
import net.dv8tion.jda.api.interactions.commands.build.Commands;
9-
11+
import net.dv8tion.jda.api.utils.FileUpload;
1012
import org.jetbrains.annotations.NotNull;
1113
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
1214

15+
import javax.annotation.Nonnull;
16+
import java.nio.charset.StandardCharsets;
1317
import java.util.List;
1418

1519
/**
@@ -27,9 +31,48 @@ public FormatAndIndentCodeMessageContext() {
2731

2832
@Override
2933
public void execute(@NotNull MessageContextInteractionEvent event) {
30-
event.replyFormat("```java\n%s\n```", IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()), IndentationHelper.IndentationType.TABS))
34+
String indented = IndentationHelper.formatIndentation(
35+
StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()),
36+
IndentationHelper.IndentationType.TABS);
37+
38+
if (indented.isBlank()) {
39+
event.reply("There is no code to format in that message.").setEphemeral(true).queue();
40+
return;
41+
}
42+
43+
Code code = new Code(Language.JAVA, indented);
44+
List<String> messages = code.toDiscordMessages();
45+
46+
// Reply with the full code as a file (acknowledges the interaction), then post
47+
// the readable code-block chunks in order.
48+
FileUpload file = FileUpload.fromData(indented.getBytes(StandardCharsets.UTF_8),
49+
"code." + code.getLanguage().getDiscordName());
50+
MessageChannel channel = event.getChannel();
51+
event.replyFiles(file)
52+
.setAllowedMentions(List.of())
53+
.queue(
54+
success -> sendChunksInOrder(channel, messages, 0, event),
55+
error -> {
56+
ExceptionLogger.capture(error, getClass().getSimpleName());
57+
Responses.error(event.getHook(), "The message could not be converted into a formatted code block.")
58+
.queue();
59+
}
60+
);
61+
}
62+
63+
private void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, @Nonnull MessageContextInteractionEvent event) {
64+
if (index >= messages.size()) {
65+
return;
66+
}
67+
channel.sendMessage(messages.get(index))
3168
.setAllowedMentions(List.of())
32-
.setComponents(FormatCodeCommand.buildActionRow(event.getTarget(), event.getUser().getIdLong()))
33-
.queue();
69+
.queue(
70+
success -> sendChunksInOrder(channel, messages, index + 1, event),
71+
error -> {
72+
ExceptionLogger.capture(error, getClass().getSimpleName());
73+
Responses.error(event.getHook(), "The message could not be converted into a formatted code block.")
74+
.queue();
75+
}
76+
);
3477
}
3578
}

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

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

3+
import net.dv8tion.jda.api.interactions.InteractionHook;
34
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
45
import net.discordjug.javabot.util.*;
56
import net.dv8tion.jda.api.components.actionrow.ActionRow;
@@ -77,10 +78,7 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
7778
.filter(m -> !m.getAuthor().isBot()).findFirst()
7879
.orElse(null);
7980
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();
81+
sendFormattedCode(event, target, format, indentation);
8482
} else {
8583
Responses.error(event.getHook(), "Could not find message; please specify a message id.").queue();
8684
}
@@ -92,11 +90,38 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
9290
}
9391
long messageId = idOption.getAsLong();
9492
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(),
93+
target -> sendFormattedCode(event, target, format, indentation),
9994
e -> Responses.error(event.getHook(), "Could not retrieve message with id: " + messageId).queue());
10095
}
10196
}
102-
}
97+
98+
private void sendFormattedCode(SlashCommandInteractionEvent event, Message target, String format, String indentation) {
99+
String content = IndentationHelper.formatIndentation(
100+
StringUtils.standardSanitizer().compute(target.getContentRaw()),
101+
IndentationHelper.IndentationType.valueOf(indentation));
102+
103+
if (content.isBlank()) {
104+
Responses.error(event.getHook(), "There is no code to format in that message.").queue();
105+
return;
106+
}
107+
108+
Code code = new Code(Language.fromString(format), content);
109+
sendChunksInOrder(event.getHook(), code.toDiscordMessages(), 0);
110+
}
111+
112+
private void sendChunksInOrder(InteractionHook hook, List<String> messages, int index) {
113+
if (index >= messages.size()) {
114+
return;
115+
}
116+
var action = hook.sendMessage(messages.get(index)).setAllowedMentions(List.of());
117+
118+
action.queue(
119+
success -> sendChunksInOrder(hook, messages, index + 1),
120+
error -> {
121+
ExceptionLogger.capture(error, getClass().getSimpleName());
122+
Responses.error(hook, "The message could not be converted into a formatted code block.")
123+
.queue();
124+
}
125+
);
126+
}
127+
}

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

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

3-
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
3+
import net.discordjug.javabot.util.ExceptionLogger;
4+
import net.discordjug.javabot.util.Responses;
45
import net.discordjug.javabot.util.StringUtils;
6+
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
7+
import net.dv8tion.jda.api.utils.FileUpload;
8+
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
59
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
610
import net.dv8tion.jda.api.interactions.InteractionContextType;
711
import net.dv8tion.jda.api.interactions.commands.build.Commands;
8-
912
import org.jetbrains.annotations.NotNull;
1013

14+
import javax.annotation.Nonnull;
15+
import java.nio.charset.StandardCharsets;
1116
import java.util.List;
1217

1318
/**
@@ -25,9 +30,57 @@ public FormatCodeMessageContext() {
2530

2631
@Override
2732
public void execute(@NotNull MessageContextInteractionEvent event) {
28-
event.replyFormat("```java\n%s\n```", StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()))
33+
String sanitized = StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw());
34+
35+
if (sanitized.isBlank()) {
36+
event.reply("There is no code to format in that message.")
37+
.setEphemeral(true)
38+
.queue();
39+
return;
40+
}
41+
42+
// Currently we always format as Java. A language dropdown will be added in the future.
43+
Code code = new Code(Language.JAVA, sanitized);
44+
List<String> messages = code.toDiscordMessages();
45+
46+
// The reply both acknowledges the interaction and hands users the full,
47+
// un-split code as a downloadable file (so chunking never loses anything).
48+
FileUpload file = FileUpload.fromData(
49+
sanitized.getBytes(StandardCharsets.UTF_8),
50+
"code." + code.getLanguage().getDiscordName()
51+
);
52+
53+
MessageChannel channel = event.getChannel();
54+
55+
event.replyFiles(file)
2956
.setAllowedMentions(List.of())
30-
.setComponents(FormatCodeCommand.buildActionRow(event.getTarget(), event.getUser().getIdLong()))
31-
.queue();
57+
.queue(
58+
success -> sendChunksInOrder(channel, messages, 0, event),
59+
error -> {
60+
ExceptionLogger.capture(error, getClass().getSimpleName());
61+
Responses.error(event.getHook(), "The message could not be converted into a formatted code block.")
62+
.queue();
63+
}
64+
);
65+
}
66+
67+
/**
68+
* Sends the code-block chunks one at a time — each in the success callback of
69+
* the previous — so Discord keeps them in order.
70+
*/
71+
private void sendChunksInOrder(MessageChannel channel, List<String> messages, int index,@Nonnull MessageContextInteractionEvent event) {
72+
if (index >= messages.size()) {
73+
return;
74+
}
75+
channel.sendMessage(messages.get(index))
76+
.setAllowedMentions(List.of()) // never ping people from pasted code
77+
.queue(
78+
success -> sendChunksInOrder(channel, messages, index + 1, event),
79+
error -> {
80+
ExceptionLogger.capture(error, getClass().getSimpleName());
81+
Responses.error(event.getHook(), "The message could not be converted into a formatted code block.")
82+
.queue();
83+
}
84+
);
3285
}
3386
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package net.discordjug.javabot.systems.user_commands.format_code;
2+
3+
public enum Language {
4+
C("c"),
5+
CPP("cpp"),
6+
CSHARP("csharp"),
7+
CSS("css"),
8+
D("d"),
9+
GO("go"),
10+
HTML("html"),
11+
JAVA("java"),
12+
JAVASCRIPT("js"),
13+
KOTLIN("kotlin"),
14+
PHP("php"),
15+
PYTHON("python"),
16+
RUBY("ruby"),
17+
RUST("rust"),
18+
SQL("sql"),
19+
SWIFT("swift"),
20+
TYPESCRIPT("typescript"),
21+
XML("xml"),
22+
UNKNOWN("txt");
23+
24+
private final String discordName;
25+
26+
Language(String discordName) {
27+
this.discordName = discordName;
28+
}
29+
30+
public String getDiscordName() {
31+
return discordName;
32+
}
33+
34+
/**
35+
* Resolves a language from a string (e.g. the value of the /format-code "format"
36+
* option) by matching its Discord code-fence name, falling back to {@link #UNKNOWN}.
37+
*
38+
* @param name the code-fence name to look up (case-insensitive)
39+
* @return the matching language, or {@link #UNKNOWN} if none matches
40+
*/
41+
public static Language fromString(String name) {
42+
for (Language language : values()) {
43+
if (language.discordName.equalsIgnoreCase(name)) {
44+
return language;
45+
}
46+
}
47+
return UNKNOWN;
48+
}
49+
}

0 commit comments

Comments
 (0)