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,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() {

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.

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

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 there a reason for this being public?

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;

/**
Expand All @@ -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();

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 you use Responses.error(event, "There is no code to format in that message.").queue();, it would show a nicer error message.
Aside from that, is there any possibility of of the string being blank here?

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

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

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();
.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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -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) {

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.

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

import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
import net.discordjug.javabot.util.ExceptionLogger;
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.utils.FileUpload;
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 javax.annotation.Nonnull;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
Expand All @@ -25,9 +30,57 @@ public FormatCodeMessageContext() {

@Override
public void execute(@NotNull MessageContextInteractionEvent event) {
event.replyFormat("```java\n%s\n```", StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()))
String sanitized = StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw());

if (sanitized.isBlank()) {
event.reply("There is no code to format in that message.")
.setEphemeral(true)
.queue();
return;
}

// Currently we always format as Java. A language dropdown will be added in the future.
Code code = new Code(Language.JAVA, sanitized);
List<String> messages = code.toDiscordMessages();

// 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(
sanitized.getBytes(StandardCharsets.UTF_8),
"code." + code.getLanguage().getDiscordName()
);

MessageChannel channel = event.getChannel();

event.replyFiles(file)
.setAllowedMentions(List.of())
.setComponents(FormatCodeCommand.buildActionRow(event.getTarget(), event.getUser().getIdLong()))
.queue();
.queue(
success -> sendChunksInOrder(channel, messages, 0, event),
error -> {
ExceptionLogger.capture(error, getClass().getSimpleName());
Responses.error(event.getHook(), "The message could not be converted into a formatted code block.")
.queue();
}
);
}

/**
* Sends the code-block chunks one at a time — each in the success callback of
* the previous — so Discord keeps them in order.
*/
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()) // never ping people from pasted code
.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
@@ -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()) {

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.

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;
}
}
Loading