Skip to content
Merged
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,74 @@
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() {
Comment thread
danthe1st marked this conversation as resolved.
return content;
}

/**
* 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()))
Comment thread
danthe1st marked this conversation as resolved.
.queue();
String indented = IndentationHelper.formatIndentation(
StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()),
IndentationHelper.IndentationType.TABS);

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

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

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;
import org.jetbrains.annotations.NotNull;

/**
* <h3>This class represents the /format-code command.</h3>
Expand All @@ -29,25 +24,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,47 +33,66 @@ 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)
.queue(messages -> {
Collections.reverse(messages);
Message target = messages.stream()
.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();
} else {
Responses.error(event.getHook(), "Could not find message; please specify a message id.").queue();
}
});
event.deferReply().queue(_ -> {
event.getChannel().getHistory()
.retrievePast(10)
.queue(messages -> {
Message target = messages.stream()
.filter(m -> !m.getAuthor().isBot()).findFirst()
.orElse(null);
if (target != null) {
sendFormattedCode(event, target, language, indentation);
} else {
Responses.errorWithTitle(event.getHook(), "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());
event.deferReply().queue(_ -> {
event.getChannel().retrieveMessageById(messageId).queue(
target -> sendFormattedCode(event, target, language, indentation),
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());
});
}
}

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,99 @@
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 org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nonnull;
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 results in an error.
*/
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.getHook(), "404 Code not found","There is no code to format in that message.").queue();
return;
}

List<String> messages = code.toDiscordMessages();
Comment thread
danthe1st marked this conversation as resolved.

MessageChannel channel = target.getChannel();

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

Responses.success(event.getHook(), "Success", "The formatted message is being sent to this channel.")
.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;
}
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);

event.deferReply().queue(_ -> FormatCodeDispatcher.sendCode(code, event, event.getTarget()));
}
}
Loading