diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index 673780d436..60a1d84b28 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -65,6 +65,7 @@ public static void main(final String[] args) { * @param token the Discord Bot token to connect with * @param databasePath the path to the database to use */ + @SuppressWarnings("WeakerAccess") public static void runBot(String token, Path databasePath) { logger.info("Starting bot..."); try { diff --git a/application/src/main/java/org/togetherjava/tjbot/BootstrapLauncher.java b/application/src/main/java/org/togetherjava/tjbot/BootstrapLauncher.java index 6b21b147e6..0be2bb353a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/BootstrapLauncher.java +++ b/application/src/main/java/org/togetherjava/tjbot/BootstrapLauncher.java @@ -5,9 +5,13 @@ * main logic to take over. */ public enum BootstrapLauncher { - ; + /** + * Starts the main application. + * + * @param args arguments are forwarded, see {@link Application#main(String[])} + */ public static void main(String[] args) { setSystemProperties(); @@ -29,6 +33,7 @@ private static void setSystemProperties() { // NOTE This will likely be fixed with Java 18 or newer, remove afterwards (see // https://bugs.openjdk.java.net/browse/JDK-8274349 and // https://github.com/openjdk/jdk/pull/5784) + // noinspection UseOfSystemOutOrSystemErr System.out.println("Available Cores \"" + cores + "\", setting Parallelism Flag"); // noinspection AccessOfSystemProperties System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "1"); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java index f05083e745..3961b19ed8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java @@ -4,6 +4,10 @@ import org.togetherjava.tjbot.commands.basic.DatabaseCommand; import org.togetherjava.tjbot.commands.basic.PingCommand; import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; +import org.togetherjava.tjbot.commands.tags.TagCommand; +import org.togetherjava.tjbot.commands.tags.TagManageCommand; +import org.togetherjava.tjbot.commands.tags.TagSystem; +import org.togetherjava.tjbot.commands.tags.TagsCommand; import org.togetherjava.tjbot.db.Database; import java.util.Collection; @@ -27,14 +31,16 @@ public enum Commands { * generally should be avoided. * * @param database the database of the application, which commands can use to persist data - * * @return a collection of all slash commands */ public static @NotNull Collection createSlashCommands( @NotNull Database database) { + TagSystem tagSystem = new TagSystem(database); // NOTE The command system can add special system relevant commands also by itself, // hence this list may not necessarily represent the full list of all commands actually // available. - return List.of(new PingCommand(), new DatabaseCommand(database), new TeXCommand()); + return List.of(new PingCommand(), new DatabaseCommand(database), new TeXCommand(), + new TagCommand(tagSystem), new TagManageCommand(tagSystem), + new TagsCommand(tagSystem)); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java index dd88e90f9a..5f8b2d6c25 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java @@ -113,7 +113,7 @@ public interface SlashCommand { /** * Triggered by the command system when a slash command corresponding to this implementation - * (based on {@link #getData()} has been triggered. + * (based on {@link #getData()}) has been triggered. *

* This method may be called multi-threaded. In particular, there are no guarantees that it will * be executed on the same thread repeatedly or on the same thread that other event methods have @@ -162,7 +162,7 @@ public interface SlashCommand { /** * Triggered by the command system when a button corresponding to this implementation (based on - * {@link #getData()} has been clicked. + * {@link #getData()}) has been clicked. *

* This method may be called multi-threaded. In particular, there are no guarantees that it will * be executed on the same thread repeatedly or on the same thread that other event methods have @@ -182,7 +182,7 @@ public interface SlashCommand { /** * Triggered by the command system when a selection menu corresponding to this implementation - * (based on {@link #getData()} has been clicked. + * (based on {@link #getData()}) has been clicked. *

* This method may be called multi-threaded. In particular, there are no guarantees that it will * be executed on the same thread repeatedly or on the same thread that other event methods have diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java index a698611196..ef0f894b99 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java @@ -23,7 +23,7 @@ *

*

* The adapter manages all command related data itself, which can be provided during construction - * (see {@link #SlashCommandAdapter(String, String, SlashCommandVisibility)}. In order to add + * (see {@link #SlashCommandAdapter(String, String, SlashCommandVisibility)}). In order to add * options, subcommands or similar command configurations, use {@link #getData()} and mutate the * returned data object (see {@link CommandData} for details on how to work with this class). *

diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/ReloadCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/ReloadCommand.java index 7254daf3b9..33613c797a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/ReloadCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/ReloadCommand.java @@ -88,9 +88,7 @@ public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List ButtonStyle buttonStyle = Objects.requireNonNull(event.getButton()).getStyle(); switch (buttonStyle) { - case DANGER -> { - event.reply("Okay, will not reload.").queue(); - } + case DANGER -> event.reply("Okay, will not reload.").queue(); case SUCCESS -> { logger.info("Reloading commands, triggered by user '{}' in guild '{}'", userId, event.getGuild()); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java new file mode 100644 index 0000000000..e82b1ad827 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java @@ -0,0 +1,51 @@ +package org.togetherjava.tjbot.commands.tags; + +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.commands.utils.MessageUtils; + +import java.util.Objects; + +/** + * Implements the {@code /tag} command which lets the bot respond content of a tag that has been + * added previously. + *

+ * Tags can be added by using {@link TagManageCommand} and a list of all tags is available using + * {@link TagsCommand}. + */ +public final class TagCommand extends SlashCommandAdapter { + private final TagSystem tagSystem; + + private static final String ID_OPTION = "id"; + + /** + * Creates a new instance, using the given tag system as base. + * + * @param tagSystem the system providing the actual tag data + */ + public TagCommand(TagSystem tagSystem) { + super("tag", "Display a tags content", SlashCommandVisibility.GUILD); + + this.tagSystem = tagSystem; + + // TODO Thing about adding an ephemeral selection menu with pagination support + // if the user calls this without id or similar + getData().addOption(OptionType.STRING, ID_OPTION, "the id of the tag to display", true); + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); + if (tagSystem.isUnknownTagAndHandle(id, event)) { + return; + } + + event + .replyEmbeds(MessageUtils.generateEmbed(null, tagSystem.getTag(id).orElseThrow(), + event.getUser(), TagSystem.AMBIENT_COLOR)) + .queue(); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagContentStyle.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagContentStyle.java new file mode 100644 index 0000000000..898576a394 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagContentStyle.java @@ -0,0 +1,18 @@ +package org.togetherjava.tjbot.commands.tags; + +/** + * The style of a tag content. + */ +public enum TagContentStyle { + /** + * Content that will be interpreted by Discord, for example a message containing {@code **foo**} + * will be displayed in bold. + */ + INTERPRETED, + /** + * Content that will be displayed raw, not interpreted by Discord. For example a message + * containing {@code **foo**} will be displayed as {@code **foo**} literally, by escaping the + * special characters. + */ + RAW +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java new file mode 100644 index 0000000000..cff16854ef --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java @@ -0,0 +1,310 @@ +package org.togetherjava.tjbot.commands.tags; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.interactions.Interaction; +import net.dv8tion.jda.api.interactions.commands.CommandInteraction; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.requests.ErrorResponse; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.commands.utils.MessageUtils; + +import java.util.Objects; +import java.util.OptionalLong; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Implements the {@code /tag-manage} command which allows management of tags, such as creating, + * editing or deleting them. Available subcommands are: + *

+ *

+ * Tags can be added by using {@link TagManageCommand} and a list of all tags is available using + * {@link TagsCommand}. + */ +public final class TagManageCommand extends SlashCommandAdapter { + private static final Logger logger = LoggerFactory.getLogger(TagManageCommand.class); + private static final String ID_OPTION = "id"; + private static final String ID_DESCRIPTION = "the id of the tag"; + private static final String CONTENT_OPTION = "content"; + private static final String CONTENT_DESCRIPTION = "the content of the tag"; + private static final String MESSAGE_ID_OPTION = "message-id"; + private static final String MESSAGE_ID_DESCRIPTION = "the id of the message to refer to"; + private final TagSystem tagSystem; + + /** + * Creates a new instance, using the given tag system as base. + * + * @param tagSystem the system providing the actual tag data + */ + public TagManageCommand(TagSystem tagSystem) { + super("tag-manage", "Provides commands to manage all tags", SlashCommandVisibility.GUILD); + + this.tagSystem = tagSystem; + + // TODO Think about adding a "Are you sure"-dialog to 'edit', 'edit-with-message' and + // 'delete' + getData().addSubcommands(new SubcommandData(Subcommand.RAW.name, + "View the raw content of a tag, without Discord interpreting any of its content") + .addOption(OptionType.STRING, ID_OPTION, ID_DESCRIPTION, true), + new SubcommandData(Subcommand.CREATE.name, "Creates a new tag") + .addOption(OptionType.STRING, ID_OPTION, ID_DESCRIPTION, true) + .addOption(OptionType.STRING, CONTENT_OPTION, CONTENT_DESCRIPTION, true), + new SubcommandData(Subcommand.CREATE_WITH_MESSAGE.name, + "Creates a new tag. Content is retrieved from the given message.") + .addOption(OptionType.STRING, ID_OPTION, ID_DESCRIPTION, true) + .addOption(OptionType.STRING, MESSAGE_ID_OPTION, MESSAGE_ID_DESCRIPTION, + true), + new SubcommandData(Subcommand.EDIT.name, "Edits a tag, the old content is replaced") + .addOption(OptionType.STRING, ID_OPTION, ID_DESCRIPTION, true) + .addOption(OptionType.STRING, CONTENT_OPTION, CONTENT_DESCRIPTION, true), + new SubcommandData(Subcommand.EDIT_WITH_MESSAGE.name, + "Edits a tag, the old content is replaced. Content is retrieved from the given message.") + .addOption(OptionType.STRING, ID_OPTION, ID_DESCRIPTION, true) + .addOption(OptionType.STRING, MESSAGE_ID_OPTION, MESSAGE_ID_DESCRIPTION, + true), + new SubcommandData(Subcommand.DELETE.name, "Deletes a tag") + .addOption(OptionType.STRING, ID_OPTION, ID_DESCRIPTION, true)); + } + + private static void sendSuccessMessage(@NotNull Interaction event, @NotNull String id, + @NotNull String actionVerb) { + logger.info("User '{}' {} the tag with id '{}'.", event.getUser().getId(), actionVerb, id); + event.replyEmbeds(MessageUtils.generateEmbed("Success", + "Successfully %s tag '%s'.".formatted(actionVerb, id), event.getUser(), + TagSystem.AMBIENT_COLOR)) + .queue(); + } + + /** + * Attempts to parse the given message id. + *

+ * If the message id could not be parsed, because it is invalid, an error message is send to the + * user. + * + * @param messageId the message id to parse + * @param event the event to send messages with + * @return the parsed message id, if successful + */ + private static OptionalLong parseMessageIdAndHandle(@NotNull String messageId, + @NotNull Interaction event) { + try { + return OptionalLong.of(Long.parseLong(messageId)); + } catch (NumberFormatException e) { + event + .reply("The given message id '%s' is invalid, expected a number." + .formatted(messageId)) + .setEphemeral(true) + .queue(); + return OptionalLong.empty(); + } + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + Member member = Objects.requireNonNull(event.getMember()); + + if (!member.hasPermission(Permission.MESSAGE_MANAGE)) { + event.reply( + "Tags can only be managed by users who have the 'MESSAGE_MANAGE' permission.") + .setEphemeral(true) + .queue(); + return; + } + + switch (Subcommand.fromName(event.getSubcommandName())) { + case RAW -> rawTag(event); + case CREATE -> createTag(event); + case CREATE_WITH_MESSAGE -> createTagWithMessage(event); + case EDIT -> editTag(event); + case EDIT_WITH_MESSAGE -> editTagWithMessage(event); + case DELETE -> deleteTag(event); + default -> throw new AssertionError( + "Unexpected subcommand '%s'".formatted(event.getSubcommandName())); + } + } + + private void rawTag(@NotNull SlashCommandEvent event) { + String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); + if (tagSystem.isUnknownTagAndHandle(id, event)) { + return; + } + + event.replyEmbeds(MessageUtils.generateEmbed(null, + MessageUtils.escapeMarkdown(tagSystem.getTag(id).orElseThrow()), event.getUser(), + TagSystem.AMBIENT_COLOR)) + .queue(); + } + + private void createTag(@NotNull CommandInteraction event) { + String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString(); + + handleAction(TagStatus.NOT_EXISTS, id -> tagSystem.putTag(id, content), "created", event); + } + + private void createTagWithMessage(@NotNull CommandInteraction event) { + handleActionWithMessage(TagStatus.NOT_EXISTS, tagSystem::putTag, "created", event); + } + + private void editTag(@NotNull CommandInteraction event) { + String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString(); + + handleAction(TagStatus.EXISTS, id -> tagSystem.putTag(id, content), "edited", event); + } + + private void editTagWithMessage(@NotNull CommandInteraction event) { + handleActionWithMessage(TagStatus.EXISTS, tagSystem::putTag, "edited", event); + } + + private void deleteTag(@NotNull CommandInteraction event) { + handleAction(TagStatus.EXISTS, tagSystem::deleteTag, "deleted", event); + } + + /** + * Executes the given action on the tag id and sends a success message to the user. + *

+ * If the tag status does not line up with the required status, an error message is send to the + * user. + * + * @param requiredTagStatus the required status of the tag + * @param idAction the action to perform on the id + * @param actionVerb the verb describing the executed action, i.e. edited or + * created, will be displayed in the message send to the user + * @param event the event to send messages with, it must have an {@code id} option set + */ + private void handleAction(@NotNull TagStatus requiredTagStatus, + @NotNull Consumer idAction, @NotNull String actionVerb, + @NotNull CommandInteraction event) { + String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); + if (isWrongTagStatusAndHandle(requiredTagStatus, id, event)) { + return; + } + + idAction.accept(id); + sendSuccessMessage(event, id, actionVerb); + } + + /** + * Executes the given action on the tag id and the content and sends a success message to the + * user. + *

+ * The content is retrieved by looking up the message with the id stored in the event. + *

+ * If the tag status does not line up with the required status or a message with the given id + * does not exist, an error message is send to the user. + * + * @param requiredTagStatus the required status of the tag + * @param idAndContentAction the action to perform on the id and content + * @param actionVerb the verb describing the executed action, i.e. edited or + * created, will be displayed in the message send to the user + * @param event the event to send messages with, it must have an {@code id} and + * {@code message-id} option set + */ + private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus, + @NotNull BiConsumer idAndContentAction, + @NotNull String actionVerb, @NotNull CommandInteraction event) { + String tagId = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); + OptionalLong messageIdOpt = parseMessageIdAndHandle( + Objects.requireNonNull(event.getOption(MESSAGE_ID_OPTION)).getAsString(), event); + if (messageIdOpt.isEmpty()) { + return; + } + long messageId = messageIdOpt.orElseThrow(); + if (isWrongTagStatusAndHandle(requiredTagStatus, tagId, event)) { + return; + } + + event.getMessageChannel().retrieveMessageById(messageId).queue(message -> { + idAndContentAction.accept(tagId, message.getContentRaw()); + sendSuccessMessage(event, tagId, actionVerb); + }, failure -> { + if (failure instanceof ErrorResponseException ex + && ex.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) { + event.reply("The message with id '%d' does not exist.".formatted(messageId)) + .setEphemeral(true) + .queue(); + return; + } + + logger.warn("Unable to retrieve the message with id '{}' for an unknown reason.", + messageId, failure); + event + .reply("Something unexpected went wrong trying to locate the message with id '%d'." + .formatted(messageId)) + .setEphemeral(true) + .queue(); + }); + } + + /** + * Returns whether the status of the given tag is not equal to the required status. + *

+ * If not, it sends an error message to the user. + * + * @param requiredTagStatus the required status of the tag + * @param id the id of the tag to check + * @param event the event to send messages with + * @return whether the status of the given tag is not equal to the required status + */ + private boolean isWrongTagStatusAndHandle(@NotNull TagStatus requiredTagStatus, + @NotNull String id, @NotNull Interaction event) { + if (requiredTagStatus == TagStatus.EXISTS) { + return tagSystem.isUnknownTagAndHandle(id, event); + } else if (requiredTagStatus == TagStatus.NOT_EXISTS) { + if (tagSystem.hasTag(id)) { + event.reply("The tag with id '%s' already exists.".formatted(id)) + .setEphemeral(true) + .queue(); + return true; + } + } else { + throw new AssertionError("Unknown tag status '%s'".formatted(requiredTagStatus)); + } + return false; + } + + private enum TagStatus { + EXISTS, + NOT_EXISTS + } + + + private enum Subcommand { + RAW("raw"), + CREATE("create"), + CREATE_WITH_MESSAGE("create-with-message"), + EDIT("edit"), + EDIT_WITH_MESSAGE("edit-with-message"), + DELETE("delete"); + + private final String name; + + Subcommand(String name) { + this.name = name; + } + + static Subcommand fromName(String name) { + for (Subcommand subcommand : Subcommand.values()) { + if (subcommand.name.equals(name)) { + return subcommand; + } + } + throw new IllegalArgumentException( + "Subcommand with name '%s' is unknown".formatted(name)); + } + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java new file mode 100644 index 0000000000..b2698ea52a --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java @@ -0,0 +1,154 @@ +package org.togetherjava.tjbot.commands.tags; + +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.interactions.Interaction; +import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; +import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.Tags; +import org.togetherjava.tjbot.db.generated.tables.records.TagsRecord; + +import java.awt.*; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * The core of the tag system. Provides methods to read and create tags, directly tied to the + * underlying database. + */ +public final class TagSystem { + /** + * The ambient color to use for tag system related messages. + */ + static final Color AMBIENT_COLOR = Color.decode("#FA8072"); + + private final Database database; + + /** + * Creates an instance. + * + * @param database the database to store and retrieve tags from + */ + public TagSystem(Database database) { + this.database = database; + } + + /** + * Creates a delete button with the given component id that can be used in message dialogs. For + * example to delete a message. + * + * @param componentId the component id to use for the button + * @return the created delete button + */ + @SuppressWarnings("StaticMethodOnlyUsedInOneClass") + static Button createDeleteButton(String componentId) { + return Button.of(ButtonStyle.DANGER, componentId, "Delete", + Emoji.fromUnicode("\uD83D\uDDD1")); // trash bin + } + + /** + * Returns whether the given tag is unknown to the system. + *

+ * If it is unknown, it sends an error message to the user. + * + * @param id the id of the tag to check + * @param event the event to send messages with + * @return whether the given tag is unknown to the system + */ + boolean isUnknownTagAndHandle(@NotNull String id, @NotNull Interaction event) { + if (hasTag(id)) { + return false; + } + // TODO Add fuzzy string matching suggestions (Levenshtein edit distance) + event.reply("Could not find any tag with id '%s'.".formatted(id)) + .setEphemeral(true) + .queue(); + return true; + } + + /** + * Checks if the given tag is known to the tag system. + * + * @param id the id of the tag to check + * @return whether the tag is known to the tag system + */ + boolean hasTag(String id) { + return database.readTransaction(context -> { + try (var selectFrom = context.selectFrom(Tags.TAGS)) { + return selectFrom.where(Tags.TAGS.ID.eq(id)).fetchOne() != null; + } + }); + } + + /** + * Deletes a tag from the tag system. + * + * @param id the id of the tag to delete + * @throws IllegalArgumentException if the tag is unknown to the system, see + * {@link #hasTag(String)} + */ + // Execute closes resources; without curly braces on the lambda, the call would be ambiguous + @SuppressWarnings({"resource", "java:S1602"}) + void deleteTag(String id) { + int deletedRecords = database.write(context -> { + return context.deleteFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(id)).execute(); + }); + if (deletedRecords == 0) { + throw new IllegalArgumentException( + "Unable to delete the tag '%s', it is unknown to the system".formatted(id)); + } + } + + /** + * Inserts or replaces the tag with the given data into the system. + * + * @param id the id of the tag to put + * @param content the content of the tag to put + */ + // Execute closes resources; without curly braces on the lambda, the call would be ambiguous + @SuppressWarnings({"resource", "java:S1602"}) + void putTag(String id, String content) { + database.writeTransaction(context -> { + context.insertInto(Tags.TAGS, Tags.TAGS.ID, Tags.TAGS.CONTENT) + .values(id, content) + .onDuplicateKeyUpdate() + .set(Tags.TAGS.CONTENT, content) + .execute(); + }); + } + + /** + * Retrieves the content of the given tag, if it is known to the system (see + * {@link #hasTag(String)}). + * + * @param id the id of the tag to get + * @return the content of the tag, if the tag is known to the system + */ + Optional getTag(String id) { + return database.readTransaction(context -> { + try (var selectFrom = context.selectFrom(Tags.TAGS)) { + return Optional.ofNullable(selectFrom.where(Tags.TAGS.ID.eq(id)).fetchOne()) + .map(TagsRecord::getContent); + } + }); + } + + /** + * Gets the ids of all tags known to the system. + * + * @return a set of all ids known to the system, not backed + */ + Set getAllIds() { + return database.readTransaction(context -> { + try (var select = context.select(Tags.TAGS.ID)) { + return select.from(Tags.TAGS) + .fetch() + .stream() + .map(dbRecord -> dbRecord.getValue(Tags.TAGS.ID)) + .collect(Collectors.toSet()); + } + }); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java new file mode 100644 index 0000000000..82baa36a94 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java @@ -0,0 +1,67 @@ +package org.togetherjava.tjbot.commands.tags; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.commands.utils.MessageUtils; + +import java.util.List; +import java.util.Objects; + +/** + * Implements the {@code /tags} command which lets the bot respond with all available tags. + *

+ * Tags can be added by using {@link TagManageCommand} and viewed by {@link TagCommand}. + *

+ * For example, suppose there is a tag with id {@code foo} and content {@code bar}, then: + * + *

+ * {@code
+ * /tag foo
+ * // TJ-Bot: bar
+ * }
+ * 
+ */ +public final class TagsCommand extends SlashCommandAdapter { + private final TagSystem tagSystem; + + /** + * Creates a new instance, using the given tag system as base. + * + * @param tagSystem the system providing the actual tag data + */ + public TagsCommand(TagSystem tagSystem) { + super("tags", "Displays all available tags", SlashCommandVisibility.GUILD); + + this.tagSystem = tagSystem; + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + // TODO A list might be better than comma separated, which is hard to read + event.replyEmbeds(MessageUtils.generateEmbed("All available tags", + String.join(", ", tagSystem.getAllIds()), event.getUser(), TagSystem.AMBIENT_COLOR)) + .addActionRow( + TagSystem.createDeleteButton(generateComponentId(event.getUser().getId()))) + .queue(); + } + + @Override + public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { + String userId = args.get(0); + + if (!event.getUser().getId().equals(userId) && !Objects.requireNonNull(event.getMember()) + .hasPermission(Permission.MESSAGE_MANAGE)) { + event.reply( + "The message can only be deleted by its author or an user with 'MESSAGE_MANAGE' permissions.") + .setEphemeral(true) + .queue(); + return; + } + + event.getMessage().delete().queue(); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/package-info.java new file mode 100644 index 0000000000..e250c06ae9 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/package-info.java @@ -0,0 +1,7 @@ +/** + * This package offers the tag system and its commands. See + * {@link org.togetherjava.tjbot.commands.tags.TagSystem} for the core of the system and commands + * like {@link org.togetherjava.tjbot.commands.tags.TagCommand} as entry point to the package's + * offered functionality. + */ +package org.togetherjava.tjbot.commands.tags; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/utils/MessageUtils.java b/application/src/main/java/org/togetherjava/tjbot/commands/utils/MessageUtils.java index 83feddad84..ce35d8789d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/utils/MessageUtils.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/utils/MessageUtils.java @@ -1,10 +1,17 @@ package org.togetherjava.tjbot.commands.utils; +import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.utils.MarkdownSanitizer; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.awt.*; +import java.time.Instant; import java.util.List; /** @@ -13,16 +20,13 @@ * This class is meant to contain all utility methods for {@link Message} that can be used on all * other commands to avoid similar methods appearing everywhere. */ -public class MessageUtils { - - private MessageUtils() { - throw new UnsupportedOperationException(); - } +public enum MessageUtils { + ; /** * Disables all the buttons that a message has. Disabling buttons deems it as not clickable to * the user who sees it. - *

+ *

* This method already queues the changes for you and does not block in any way. * * @param message the message that contains at least one button @@ -39,4 +43,38 @@ public static void disableButtons(@NotNull Message message) { .queue(); } + /** + * Generates an embed with the given content. + * + * @param title title of the embed or {@code null} if not desired + * @param content content to display in the embed + * @param user name of the user who requested the embed or {@code null} if no user requested + * this + * @param ambientColor the ambient color of the embed or {@code null} for a default color + * @return the generated embed + */ + public static @NotNull MessageEmbed generateEmbed(@Nullable String title, + @NotNull CharSequence content, @Nullable User user, @Nullable Color ambientColor) { + return new EmbedBuilder().setTitle(title) + .setDescription(content) + .setTimestamp(Instant.now()) + .setFooter(user == null ? null : user.getName()) + .setColor(ambientColor) + .build(); + } + + /** + * Escapes every markdown content in the given string. + * + * If the escaped message is sent to Discord, it will display the original message. + * + * @param text the text to escape + * @return the escaped text + */ + public static @NotNull String escapeMarkdown(@NotNull String text) { + // NOTE Unfortunately the utility does not escape backslashes '\', so we have to do it + // ourselves + return MarkdownSanitizer.escape(text.replace("\\", "\\\\")); + } + } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/utils/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/utils/package-info.java new file mode 100644 index 0000000000..b51bd42a90 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/utils/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains general utility used by commands. + */ +package org.togetherjava.tjbot.commands.utils; diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 96ca7ca368..88097957f4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -16,6 +16,7 @@ @SuppressWarnings({"Singleton", "ClassCanBeRecord"}) public final class Config { + @SuppressWarnings("RedundantFieldInitialization") private static Config config = null; private final String token; diff --git a/application/src/main/resources/db/V2__Add_Tag_System.sql b/application/src/main/resources/db/V2__Add_Tag_System.sql new file mode 100644 index 0000000000..b33cb48539 --- /dev/null +++ b/application/src/main/resources/db/V2__Add_Tag_System.sql @@ -0,0 +1,5 @@ +CREATE TABLE tags +( + id TEXT NOT NULL PRIMARY KEY, + content TEXT NOT NULL +) diff --git a/application/src/test/java/org/togetherjava/tjbot/commands/utils/MessageUtilsTest.java b/application/src/test/java/org/togetherjava/tjbot/commands/utils/MessageUtilsTest.java new file mode 100644 index 0000000000..f5170c4235 --- /dev/null +++ b/application/src/test/java/org/togetherjava/tjbot/commands/utils/MessageUtilsTest.java @@ -0,0 +1,57 @@ +package org.togetherjava.tjbot.commands.utils; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class MessageUtilsTest { + + @Test + void escapeMarkdown() { + List tests = List.of(new TestCase("empty", "", ""), + new TestCase("no markdown", "hello world", "hello world"), + new TestCase("basic markdown", "\\*\\*hello\\*\\* \\_world\\_", + "**hello** _world_"), + new TestCase("code block", """ + \\```java + int x = 5; + \\``` + """, """ + ```java + int x = 5; + ``` + """), new TestCase("escape simple", "hello\\\\\\\\world\\\\\\\\test", + "hello\\\\world\\\\test"), + new TestCase("escape complex", """ + Hello\\\\\\\\world + \\```java + Hello\\\\\\\\ + world + \\``` + test out this + \\```java + "Hello \\\\" World\\\\\\\\\\\\"" haha + \\``` + """, """ + Hello\\\\world + ```java + Hello\\\\ + world + ``` + test out this + ```java + "Hello \\" World\\\\\\"" haha + ``` + """)); + + for (TestCase test : tests) { + assertEquals(test.escapedMessage(), MessageUtils.escapeMarkdown(test.originalMessage()), + "Test failed: " + test.testName()); + } + } + + private record TestCase(String testName, String escapedMessage, String originalMessage) { + } +} diff --git a/database/src/main/java/org/togetherjava/tjbot/db/DatabaseException.java b/database/src/main/java/org/togetherjava/tjbot/db/DatabaseException.java index 41705d62b0..87b2de3882 100644 --- a/database/src/main/java/org/togetherjava/tjbot/db/DatabaseException.java +++ b/database/src/main/java/org/togetherjava/tjbot/db/DatabaseException.java @@ -5,7 +5,7 @@ /** * Thrown when an error occurs while interacting with the database. */ -public class DatabaseException extends RuntimeException { +public final class DatabaseException extends RuntimeException { /** * Serial version UID. */ diff --git a/formatter/src/main/java/org/togetherjava/tjbot/formatter/CodeSectionFormatter.java b/formatter/src/main/java/org/togetherjava/tjbot/formatter/CodeSectionFormatter.java index 801bcf4f00..4ea0f1c367 100644 --- a/formatter/src/main/java/org/togetherjava/tjbot/formatter/CodeSectionFormatter.java +++ b/formatter/src/main/java/org/togetherjava/tjbot/formatter/CodeSectionFormatter.java @@ -11,8 +11,6 @@ /** * Formatter which specifically formats code tokens (that are part of a section) - * - * @author illuminator3 */ class CodeSectionFormatter { private final StringBuilder result = new StringBuilder(); @@ -37,7 +35,6 @@ class CodeSectionFormatter { * Removes all whitespaces from the given queue * * @param queue the queue to remove whitespaces from - * @author illuminator3 */ private static void purgeWhitespaces(Queue queue) { queue.removeIf(t -> t.type() == TokenType.WHITESPACE); @@ -45,8 +42,6 @@ private static void purgeWhitespaces(Queue queue) { /** * Starts the formatting process - * - * @author illuminator3 */ void format() { Token next; @@ -62,7 +57,6 @@ void format() { * Consumes the next token * * @param token token to consume - * @author illuminator3 */ private void consume(Token token) { TokenType type = token.type(); @@ -87,7 +81,6 @@ private void consume(Token token) { * Puts the next token into the result * * @param token token to put - * @author illuminator3 */ private void put(Token token) { TokenType type = token.type(); @@ -117,7 +110,6 @@ private void put(Token token) { * * @param token token to check * @return whether a space should be put after that token - * @author illuminator3 */ private boolean shouldPutSpaceAfter(Token token) { TokenType type = token.type(); @@ -138,7 +130,6 @@ private boolean shouldPutSpaceAfter(Token token) { * {@link SkippableLookaheadQueue#peek(int, Predicate)} * * @param type current token type - * @author illuminator3 */ private void checkFor(TokenType type) { if (isIndexedForLoop(type)) { // if it's a for int loop then set the forLevel to 2 @@ -150,7 +141,6 @@ private void checkFor(TokenType type) { * Handles the case of being inside a generic type declaration * * @param token current token - * @author illuminator3 */ private void handleGeneric(Token token) { TokenType type = token.type(); @@ -188,7 +178,6 @@ private void handleGeneric(Token token) { * * @param type current token type * @return whether the token type belongs to a generic type declaration - * @author illuminator3 */ private boolean checkGeneric(TokenType type) { if (type == TokenType.LESS_THAN) { @@ -227,7 +216,6 @@ private boolean checkGeneric(TokenType type) { * * @param type token type to check * @return whether it's valid inside a generic type declaration - * @author illuminator3 */ private boolean isValidGeneric(TokenType type) { return type == TokenType.WILDCARD || type == TokenType.LESS_THAN @@ -241,7 +229,6 @@ private boolean isValidGeneric(TokenType type) { * * @param type token type to check * @return whether a new line should be put after that token - * @author illuminator3 */ private boolean shouldPutNewLineAfter(TokenType type) { if (type == TokenType.OPEN_BRACES || type == TokenType.SEMICOLON @@ -261,7 +248,6 @@ private boolean shouldPutNewLineAfter(TokenType type) { * * @param type current token type * @return whether there's an indexed for loop or not - * @author illuminator3 */ private boolean isIndexedForLoop(TokenType type) { return type == TokenType.FOR && !internalEnhancedFor(); @@ -271,7 +257,6 @@ private boolean isIndexedForLoop(TokenType type) { * Checks if there's an enhanced for loop ahead without checking the current token type * * @return whether there's an enhanced for loop ahead - * @author illuminator3 */ private boolean internalEnhancedFor() { return queue.peek(3, t -> { @@ -286,7 +271,6 @@ private boolean internalEnhancedFor() { * closing parenthesis, an operator or a semicolon * * @return whether a space should be put after the parenthesis - * @author illuminator3 */ private boolean isParenthesisRule(Token token) { if (queue.isEmpty()) { @@ -303,8 +287,6 @@ private boolean isParenthesisRule(Token token) { /** * Appends a new line if there's more in the token queue - * - * @author illuminator3 */ private void appendNewLine() { if (!queue.isEmpty()) { @@ -321,7 +303,6 @@ private void appendNewLine() { * * @param token token to check * @return whether the given token is a keyword - * @author illuminator3 */ private boolean isKeyword(Token token) { return token.type().isKeyword(); @@ -332,7 +313,6 @@ private boolean isKeyword(Token token) { * * @param token token to check * @return whether the given token is an operator - * @author illuminator3 */ private boolean isOperator(Token token) { return token.type().isOperator(); @@ -342,7 +322,6 @@ private boolean isOperator(Token token) { * Updates the indentation based on the current token type * * @param type current token type - * @author illuminator3 */ private void updateIndentation(TokenType type) { if (type == TokenType.OPEN_BRACES) { @@ -354,8 +333,6 @@ private void updateIndentation(TokenType type) { /** * Applies the current indentation - * - * @author illuminator3 */ private void applyIndentation() { if (applyIndentation) { diff --git a/formatter/src/main/java/org/togetherjava/tjbot/formatter/Formatter.java b/formatter/src/main/java/org/togetherjava/tjbot/formatter/Formatter.java index 624aec1b7f..48b9f2ca25 100644 --- a/formatter/src/main/java/org/togetherjava/tjbot/formatter/Formatter.java +++ b/formatter/src/main/java/org/togetherjava/tjbot/formatter/Formatter.java @@ -10,8 +10,6 @@ /** * Formatter which can format a given string into a string which contains code blocks etc - * - * @author illuminator3 */ public class Formatter { /** @@ -19,7 +17,6 @@ public class Formatter { * * @param tokens tokens to format * @return resulting code - * @author illuminator3 */ public String format(List tokens) { List

sections = sectionize(indexTokens(tokens)); @@ -44,7 +41,6 @@ public String format(List tokens) { * @param input input to format * @param lexer lexer to use * @return resulting code - * @author illuminator3 */ public String format(String input, Lexer lexer) { return format(lexer.tokenize(input)); @@ -55,7 +51,6 @@ public String format(String input, Lexer lexer) { * * @param tokens tokens to join * @return joined form of the tokens - * @author illuminator3 */ private String joinTokens(List tokens) { return tokens.stream().map(Token::content).collect(Collectors.joining()); @@ -67,7 +62,6 @@ private String joinTokens(List tokens) { * * @param tokens tokens to write * @return written code sections - * @author illuminator3 */ private StringBuilder writeCodeSection(List tokens) { CodeSectionFormatter formatter = new CodeSectionFormatter(tokens); @@ -82,7 +76,6 @@ private StringBuilder writeCodeSection(List tokens) { * * @param tokens not-indexed tokens * @return indexed tokens - * @author illuminator3 */ private List indexTokens(List tokens) { return tokens.stream() @@ -95,7 +88,6 @@ private List indexTokens(List tokens) { * * @param token token to check * @return true if it's a code token, false if not - * @author illuminator3 */ private boolean isTokenPartOfCode(Token token) { return token.type() != TokenType.UNKNOWN; @@ -107,7 +99,6 @@ private boolean isTokenPartOfCode(Token token) { * * @param checkedTokens checked tokens * @return list of sections - * @author illuminator3 */ private List
sectionize(List checkedTokens) { CheckedToken first = checkedTokens.get(0); @@ -135,16 +126,12 @@ private List
sectionize(List checkedTokens) { /** * Section POJR - * - * @author illuminator3 */ private static record Section(List tokens, boolean isCodeSection) { } /** * CheckedToken POJR - * - * @author illuminator3 */ private static record CheckedToken(Token token, boolean isCode) { } diff --git a/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/Lexer.java b/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/Lexer.java index cbea6a7f38..9838f1dc29 100644 --- a/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/Lexer.java +++ b/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/Lexer.java @@ -8,8 +8,6 @@ /** * Tokenizer that can turn a list of strings (or a string) into a list of tokens - * - * @author illuminator3 */ public class Lexer { /** @@ -23,7 +21,6 @@ public class Lexer { * * @param input input to tokenize * @return resulting tokens - * @author illuminator3 */ public List tokenize(String input) { return tokenize(Arrays.asList(patchComments(input).split("\n"))); @@ -34,7 +31,6 @@ public List tokenize(String input) { * * @param lines input to tokenize * @return resulting tokens - * @author illuminator3 */ public List tokenize(List lines) { return lines.stream().map(this::tokenizeLine).flatMap(List::stream).toList(); @@ -45,7 +41,6 @@ public List tokenize(List lines) { * * @param line input to tokenize * @return resulting tokens - * @author illuminator3 */ private List tokenizeLine(String line) { List tokens = new ArrayList<>(); @@ -80,7 +75,6 @@ private Token findToken(String content) { * * @param input input to patch * @return resulting string - * @author illuminator3 */ private String patchComments(String input) { // fix this, you shouldn't need this! Matcher matcher = commentPatcherRegex.matcher(input); diff --git a/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/Token.java b/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/Token.java index 9d4d751256..c43d2e80a6 100644 --- a/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/Token.java +++ b/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/Token.java @@ -4,8 +4,6 @@ /** * Class representing a Token with a given content and a type - * - * @author illuminator3 */ public record Token(String content, TokenType type) { private static final Set displayTypes = @@ -20,7 +18,6 @@ public String toString() { * Returns a non-empty string if this token has something to display * * @return the displayed value - * @author illuminator3 */ private String formatForDisplay() { if (displayTypes.contains(type())) { diff --git a/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/TokenType.java b/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/TokenType.java index 3a125fea74..5aebce63c1 100644 --- a/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/TokenType.java +++ b/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/TokenType.java @@ -4,8 +4,6 @@ /** * Represents every possible token that can be parsed by the lexer - * - * @author illuminator3 */ public enum TokenType { // keywords diff --git a/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/TokenizationException.java b/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/TokenizationException.java index 1ae6c3790b..a233ed127e 100644 --- a/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/TokenizationException.java +++ b/formatter/src/main/java/org/togetherjava/tjbot/formatter/tokenizer/TokenizationException.java @@ -2,8 +2,6 @@ /** * Exception that can occur when lexing - * - * @author illuminator3 */ public class TokenizationException extends RuntimeException { public TokenizationException(String message) { diff --git a/formatter/src/main/java/org/togetherjava/tjbot/formatter/util/LookaheadArrayDeque.java b/formatter/src/main/java/org/togetherjava/tjbot/formatter/util/LookaheadArrayDeque.java index 9ed1cfdf6d..5361cbcbdc 100644 --- a/formatter/src/main/java/org/togetherjava/tjbot/formatter/util/LookaheadArrayDeque.java +++ b/formatter/src/main/java/org/togetherjava/tjbot/formatter/util/LookaheadArrayDeque.java @@ -7,8 +7,6 @@ /** * A {@link LookaheadQueue} implementation that is based on an {@link ArrayDeque} - * - * @author illuminator3 */ public class LookaheadArrayDeque extends ArrayDeque implements LookaheadQueue { public LookaheadArrayDeque() {} diff --git a/formatter/src/main/java/org/togetherjava/tjbot/formatter/util/LookaheadQueue.java b/formatter/src/main/java/org/togetherjava/tjbot/formatter/util/LookaheadQueue.java index d72a4df9d7..ec45dd0408 100644 --- a/formatter/src/main/java/org/togetherjava/tjbot/formatter/util/LookaheadQueue.java +++ b/formatter/src/main/java/org/togetherjava/tjbot/formatter/util/LookaheadQueue.java @@ -5,8 +5,6 @@ public interface LookaheadQueue extends Queue { /** * Peeks into the "future", peek(0) would be the equivalent to peek() - * - * @author illuminator3 */ E peek(int n); } diff --git a/formatter/src/test/java/org/togetherjava/tjbot/formatter/FormatterTest.java b/formatter/src/test/java/org/togetherjava/tjbot/formatter/FormatterTest.java index 3366c06edb..e94e45915d 100644 --- a/formatter/src/test/java/org/togetherjava/tjbot/formatter/FormatterTest.java +++ b/formatter/src/test/java/org/togetherjava/tjbot/formatter/FormatterTest.java @@ -8,9 +8,6 @@ import org.junit.jupiter.api.TestInstance; import org.togetherjava.tjbot.formatter.tokenizer.Lexer; -/** - * @author illuminator3 - */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class FormatterTest { Lexer lexer; diff --git a/formatter/src/test/java/org/togetherjava/tjbot/formatter/tokenizer/LexerTest.java b/formatter/src/test/java/org/togetherjava/tjbot/formatter/tokenizer/LexerTest.java index 48c3f89796..05890cbd28 100644 --- a/formatter/src/test/java/org/togetherjava/tjbot/formatter/tokenizer/LexerTest.java +++ b/formatter/src/test/java/org/togetherjava/tjbot/formatter/tokenizer/LexerTest.java @@ -10,9 +10,6 @@ import java.util.ArrayList; import java.util.List; -/** - * @author illuminator3 - */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class LexerTest { Lexer lexer; diff --git a/settings.gradle b/settings.gradle index 1ba2012448..1dd56827f2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,4 +4,3 @@ include 'application' include 'database' include 'formatter' include 'logviewer' -