-
Notifications
You must be signed in to change notification settings - Fork 25
Fix formatting of code longer than Discord's message limit #549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| 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 Language language; | ||
| private final String content; | ||
|
|
||
| public Code(Language language, String content) { | ||
| this.language = language; | ||
| this.content = content; | ||
| } | ||
|
|
||
| public String getContent() { | ||
| return content; | ||
| } | ||
|
|
||
| public Language getLanguage() { | ||
| return language; | ||
| } | ||
|
|
||
| public void setLanguage(Language language) { | ||
| this.language = 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. | ||
| */ | ||
| public List<String> toDiscordChunks() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason for this being |
||
| 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; | ||
| } | ||
|
|
||
| /** Wraps each chunk in a language-tagged Discord code block. */ | ||
| 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,15 +1,19 @@ | ||
| package net.discordjug.javabot.systems.user_commands.format_code; | ||
|
|
||
|
|
||
| import net.discordjug.javabot.util.ExceptionLogger; | ||
| import net.discordjug.javabot.util.IndentationHelper; | ||
| import net.discordjug.javabot.util.Responses; | ||
| import net.discordjug.javabot.util.StringUtils; | ||
| import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; | ||
| 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 net.dv8tion.jda.api.utils.FileUpload; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand; | ||
|
|
||
| import javax.annotation.Nonnull; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
|
|
@@ -27,9 +31,48 @@ 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)) | ||
| String indented = IndentationHelper.formatIndentation( | ||
| StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()), | ||
| IndentationHelper.IndentationType.TABS); | ||
|
|
||
| if (indented.isBlank()) { | ||
| event.reply("There is no code to format in that message.").setEphemeral(true).queue(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you use |
||
| return; | ||
| } | ||
|
|
||
| Code code = new Code(Language.JAVA, indented); | ||
| List<String> messages = code.toDiscordMessages(); | ||
|
|
||
| // Reply with the full code as a file (acknowledges the interaction), then post | ||
| // the readable code-block chunks in order. | ||
| FileUpload file = FileUpload.fromData(indented.getBytes(StandardCharsets.UTF_8), | ||
| "code." + code.getLanguage().getDiscordName()); | ||
| MessageChannel channel = event.getChannel(); | ||
| event.replyFiles(file) | ||
| .setAllowedMentions(List.of()) | ||
| .queue( | ||
| success -> sendChunksInOrder(channel, messages, 0, event), | ||
| error -> { | ||
| ExceptionLogger.capture(error, getClass().getSimpleName()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally think it isn't necessary to add error logic like this when it "should never happen". If there is no handler, JDA logs the error either way. However, it's fine if you prefer it that way. |
||
| Responses.error(event.getHook(), "The message could not be converted into a formatted code block.") | ||
| .queue(); | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| private void sendChunksInOrder(MessageChannel channel, List<String> messages, int index, @Nonnull MessageContextInteractionEvent event) { | ||
| if (index >= messages.size()) { | ||
| return; | ||
| } | ||
| channel.sendMessage(messages.get(index)) | ||
| .setAllowedMentions(List.of()) | ||
| .setComponents(FormatCodeCommand.buildActionRow(event.getTarget(), event.getUser().getIdLong())) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Here is with buttons:
as you can see it's hard to read through indentation.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it is a single message, both buttons should definitely be there. |
||
| .queue(); | ||
| .queue( | ||
| success -> sendChunksInOrder(channel, messages, index + 1, event), | ||
| error -> { | ||
| ExceptionLogger.capture(error, getClass().getSimpleName()); | ||
| Responses.error(event.getHook(), "The message could not be converted into a formatted code block.") | ||
| .queue(); | ||
| } | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| package net.discordjug.javabot.systems.user_commands.format_code; | ||
|
|
||
| import net.dv8tion.jda.api.interactions.InteractionHook; | ||
| import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand; | ||
| import net.discordjug.javabot.util.*; | ||
| import net.dv8tion.jda.api.components.actionrow.ActionRow; | ||
|
|
@@ -77,10 +78,7 @@ 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, format, indentation); | ||
| } else { | ||
| Responses.error(event.getHook(), "Could not find message; please specify a message id.").queue(); | ||
| } | ||
|
|
@@ -92,11 +90,38 @@ public void execute(@NotNull SlashCommandInteractionEvent event) { | |
| } | ||
| 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(), | ||
| target -> sendFormattedCode(event, target, format, indentation), | ||
| e -> Responses.error(event.getHook(), "Could not retrieve message with id: " + messageId).queue()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void sendFormattedCode(SlashCommandInteractionEvent event, Message target, String format, String indentation) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is quite a bit of code duplication between these methods and the ones in the other classes. Please refactor that to only have the common logic once. |
||
| String content = IndentationHelper.formatIndentation( | ||
| StringUtils.standardSanitizer().compute(target.getContentRaw()), | ||
| IndentationHelper.IndentationType.valueOf(indentation)); | ||
|
|
||
| if (content.isBlank()) { | ||
| Responses.error(event.getHook(), "There is no code to format in that message.").queue(); | ||
| return; | ||
| } | ||
|
|
||
| Code code = new Code(Language.fromString(format), content); | ||
| sendChunksInOrder(event.getHook(), code.toDiscordMessages(), 0); | ||
| } | ||
|
|
||
| private void sendChunksInOrder(InteractionHook hook, List<String> messages, int index) { | ||
| if (index >= messages.size()) { | ||
| return; | ||
| } | ||
| var action = hook.sendMessage(messages.get(index)).setAllowedMentions(List.of()); | ||
|
|
||
| action.queue( | ||
| success -> sendChunksInOrder(hook, messages, index + 1), | ||
| error -> { | ||
| ExceptionLogger.capture(error, getClass().getSimpleName()); | ||
| Responses.error(hook, "The message could not be converted into a formatted code block.") | ||
| .queue(); | ||
| } | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package net.discordjug.javabot.systems.user_commands.format_code; | ||
|
|
||
| public enum Language { | ||
| C("c"), | ||
| CPP("cpp"), | ||
| CSHARP("csharp"), | ||
| CSS("css"), | ||
| D("d"), | ||
| GO("go"), | ||
| HTML("html"), | ||
| JAVA("java"), | ||
| JAVASCRIPT("js"), | ||
| KOTLIN("kotlin"), | ||
| PHP("php"), | ||
| PYTHON("python"), | ||
| RUBY("ruby"), | ||
| RUST("rust"), | ||
| SQL("sql"), | ||
| SWIFT("swift"), | ||
| TYPESCRIPT("typescript"), | ||
| XML("xml"), | ||
| UNKNOWN("txt"); | ||
|
|
||
| private final String discordName; | ||
|
|
||
| Language(String discordName) { | ||
| this.discordName = discordName; | ||
| } | ||
|
|
||
| public String getDiscordName() { | ||
| return discordName; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves a language from a string (e.g. the value of the /format-code "format" | ||
| * option) by matching its Discord code-fence name, falling back to {@link #UNKNOWN}. | ||
| * | ||
| * @param name the code-fence name to look up (case-insensitive) | ||
| * @return the matching language, or {@link #UNKNOWN} if none matches | ||
| */ | ||
| public static Language fromString(String name) { | ||
| for (Language language : values()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of this loop, you could use something like this: try {
return Language.valueOf(name.toUpperCase());
} catch (IllegalArgumentException e) {
return UNKNOWN;
} |
||
| if (language.discordName.equalsIgnoreCase(name)) { | ||
| return language; | ||
| } | ||
| } | ||
| return UNKNOWN; | ||
| } | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think
getContent()andsetLanguageare unused. If so, please remove them.