From f41afc6ab89f5dec35eea5b8962cc1f889e3bb8a Mon Sep 17 00:00:00 2001 From: illuminator3 Date: Thu, 14 Oct 2021 10:48:44 +0200 Subject: [PATCH 01/12] Added tag system --- .../togetherjava/tjbot/commands/Commands.java | 9 +- .../tjbot/commands/tag/TagCommand.java | 102 ++++++++ .../tjbot/commands/tag/TagManageCommand.java | 245 ++++++++++++++++++ .../tjbot/commands/tag/TagSystem.java | 110 ++++++++ .../tjbot/commands/tag/TagUtility.java | 121 +++++++++ .../tjbot/commands/tag/TagsCommand.java | 52 ++++ .../tjbot/commands/utils/MessageUtils.java | 29 +++ .../main/resources/db/V2__Add_Tag_System.sql | 5 + .../tjbot/db/DatabaseException.java | 2 +- settings.gradle | 1 - 10 files changed, 672 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java create mode 100644 application/src/main/resources/db/V2__Add_Tag_System.sql 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..09603ead9c 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.tag.TagCommand; +import org.togetherjava.tjbot.commands.tag.TagManageCommand; +import org.togetherjava.tjbot.commands.tag.TagSystem; +import org.togetherjava.tjbot.commands.tag.TagsCommand; import org.togetherjava.tjbot.db.Database; import java.util.Collection; @@ -27,14 +31,15 @@ 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/tag/TagCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java new file mode 100644 index 0000000000..077ca5f9fd --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java @@ -0,0 +1,102 @@ +package org.togetherjava.tjbot.commands.tag; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; +import net.dv8tion.jda.api.events.interaction.SelectionMenuEvent; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; +import net.dv8tion.jda.api.interactions.components.selections.SelectOption; +import net.dv8tion.jda.api.interactions.components.selections.SelectionMenu; +import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; + +import java.util.List; + +/** + * Tag command. Usage: {@code /tag (id) (raw)}
+ * Example disord usages:
+ * {@code /tag}
+ * {@code /tag ask}
+ * {@code /tag ask true} + * + * @author illuminator3 + */ +public final class TagCommand extends SlashCommandAdapter { + private final TagSystem tagSystem; + + public TagCommand(TagSystem tagSystem) { + super("tag", "View a tag", SlashCommandVisibility.GUILD); + + this.tagSystem = tagSystem; + + getData().addOption(OptionType.STRING, "id", "Tag id") + .addOption(OptionType.BOOLEAN, "raw", "Raw"); + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + OptionMapping idOption = event.getOption("id"); + + if (idOption != null) { + String tagId = idOption.getAsString(); + + TagUtility.replyTag(event, tagId, event.getUser().getAsTag(), tagSystem, + event.getOption("raw") != null, event.getUser().getId()); + + return; + } + + SelectionMenu.Builder menu = + SelectionMenu.create(generateComponentId(event.getUser().getId())) + .setRequiredRange(1, 1); + + tagSystem.retrieveIds().stream().limit(25).forEach(tag -> menu.addOption(tag, tag)); + + event.reply("Choose a tag") + .addActionRow(menu.build()) + .addActionRow( + Button.of(ButtonStyle.DANGER, generateComponentId(event.getUser().getId()), + "Delete", Emoji.fromUnicode("\uD83D\uDDD1"))) + .queue(); + } + + @Override + public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { + String userId = args.get(0); + + if (!event.getUser().getId().equals(userId) + && !event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { + event.reply(":police_car: Button theft is not allowed").setEphemeral(true).queue(); + + return; + } + + event.getMessage().delete().queue(); + } + + @Override + public void onSelectionMenu(@NotNull SelectionMenuEvent event, @NotNull List args) { + String userId = args.get(0); + + if (!event.getUser().getId().equals(userId)) { + event.reply(":police_car: Selection menu theft is not allowed") + .setEphemeral(true) + .queue(); + + return; + } + + SelectOption option = event.getSelectedOptions().get(0); + String tagId = option.getLabel(); + + event.getMessage().delete().queue(); + + TagUtility.sendTag(event.getMessageChannel(), tagId, event.getUser().getAsTag(), tagSystem, + false, generateComponentId(userId)); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java new file mode 100644 index 0000000000..fd56089d51 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java @@ -0,0 +1,245 @@ +package org.togetherjava.tjbot.commands.tag; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; +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 java.awt.*; +import java.time.LocalDateTime; +import java.util.List; + +/** + * TagManage command. Usage: + * {@code /tagmanage [rawtag|edittag|editidtag|deletedtag|createtag|createidtag] (...)}. + * + * @author illuminator3 + */ +public final class TagManageCommand extends SlashCommandAdapter { + private static final Logger log = LoggerFactory.getLogger(TagManageCommand.class); + + private static final String TAG_ID_LITERAL = "tag-id"; + private static final String CONTENT_LITERAL = "content"; + private static final String MESSAGE_ID_LITERAL = "message-id"; + private static final String SUCCESS_LITERAL = "Success"; + private static final String TAG_DOES_NOT_EXIST = "This tag does not exist"; + private static final String TAG_ALREADY_EXISTS = "This tag already exists"; + + private final TagSystem tagSystem; + + public TagManageCommand(TagSystem tagSystem) { + super("tagmanage", "Manage the tags", SlashCommandVisibility.GUILD); + + this.tagSystem = tagSystem; + + getData().addSubcommands( + new SubcommandData("rawtag", "View a tag in its raw form") + .addOption(OptionType.STRING, "id", TAG_ID_LITERAL), + new SubcommandData("edittag", "Edit a tag") + .addOption(OptionType.STRING, "id", TAG_ID_LITERAL, true) + .addOption(OptionType.STRING, CONTENT_LITERAL, "Tag content", true), + new SubcommandData("editidtag", "Edit a tag using a message id") + .addOption(OptionType.STRING, TAG_ID_LITERAL, TAG_ID_LITERAL, true) + .addOption(OptionType.NUMBER, MESSAGE_ID_LITERAL, MESSAGE_ID_LITERAL, true), + new SubcommandData("deletetag", "Delete a tag").addOption(OptionType.STRING, "id", + TAG_ID_LITERAL, true), + new SubcommandData("createtag", "Create a tag") + .addOption(OptionType.STRING, "id", TAG_ID_LITERAL, true) + .addOption(OptionType.STRING, CONTENT_LITERAL, "Tag content", true), + new SubcommandData("createidtag", "Create a tag using a message id") + .addOption(OptionType.STRING, TAG_ID_LITERAL, TAG_ID_LITERAL, true) + .addOption(OptionType.NUMBER, MESSAGE_ID_LITERAL, MESSAGE_ID_LITERAL, true)); + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + Member member = event.getMember(); + + if (!member.hasPermission(Permission.MESSAGE_MANAGE)) { + event.reply("You need the MESSAGE_MANAGE permission to use this command!") + .setEphemeral(true) + .queue(); + + return; + } + + switch (event.getSubcommandName()) { + case "edittag" -> editTag(event); + case "editidtag" -> editIdTag(event); + case "deletetag" -> deleteTag(event, member); + case "createtag" -> createTag(event); + case "createidtag" -> createIdTag(event); + default -> throw new IllegalStateException(); + } + } + + private void editTag(@NotNull SlashCommandEvent event) { + String tagId = event.getOption("id").getAsString(); + String content = event.getOption(CONTENT_LITERAL).getAsString(); + + if (!tagSystem.exists(tagId)) { + event.reply(TAG_DOES_NOT_EXIST).setEphemeral(true).queue(); + + return; + } + + tagSystem.put(tagId, content); + + event + .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) + .setTitle(SUCCESS_LITERAL) + .setTimestamp(LocalDateTime.now()) + .setFooter(event.getUser().getAsTag()) + .setDescription("Successfully edited tag '" + tagId + "'") + .build()) + .queue(); + } + + private void editIdTag(@NotNull SlashCommandEvent event) { + String tagId = event.getOption(TAG_ID_LITERAL).getAsString(); + long messageId = event.getOption(MESSAGE_ID_LITERAL).getAsLong(); + + if (!tagSystem.exists(tagId)) { + event.reply(TAG_DOES_NOT_EXIST).setEphemeral(true).queue(); + + return; + } + + event.getMessageChannel().retrieveMessageById(messageId).queue(message -> { + tagSystem.put(tagId, message.getContentRaw()); + + event + .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) + .setTitle(SUCCESS_LITERAL) + .setTimestamp(LocalDateTime.now()) + .setFooter(event.getUser().getAsTag()) + .setDescription("Successfully edited tag '" + tagId + "'") + .build()) + .queue(); + }, failure -> { + if (failure instanceof ErrorResponseException ex + && ex.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) { + event.reply("This message doesn't exist").setEphemeral(true).queue(); + } else { + log.error("An unknown exception occurred", failure); + } + }); + } + + private void deleteTag(@NotNull SlashCommandEvent event, @NotNull Member member) { + String tagId = event.getOption("id").getAsString(); + + if (!tagSystem.exists(tagId)) { + event.reply(TAG_DOES_NOT_EXIST).setEphemeral(true).queue(); + + return; + } + + event.reply("You sure? Confirming this will delete the tag '" + tagId + "'") + .addActionRow( + Button.of(ButtonStyle.DANGER, generateComponentId(member.getId(), tagId), + "Of course!"), + Button.of(ButtonStyle.SUCCESS, generateComponentId(member.getId()), "Abort")) + .queue(); + } + + private void createTag(@NotNull SlashCommandEvent event) { + String tagId = event.getOption("id").getAsString(); + String content = event.getOption(CONTENT_LITERAL).getAsString(); + + if (tagSystem.exists(tagId)) { + event.reply(TAG_ALREADY_EXISTS).setEphemeral(true).queue(); + + return; + } + + tagSystem.put(tagId, content); + + event + .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) + .setTitle(SUCCESS_LITERAL) + .setTimestamp(LocalDateTime.now()) + .setFooter(event.getUser().getAsTag()) + .setDescription("Successfully created tag '" + tagId + "'") + .build()) + .queue(); + } + + private void createIdTag(@NotNull SlashCommandEvent event) { + String tagId = event.getOption(TAG_ID_LITERAL).getAsString(); + long messageId = event.getOption(MESSAGE_ID_LITERAL).getAsLong(); + + if (tagSystem.exists(tagId)) { + event.reply(TAG_ALREADY_EXISTS).setEphemeral(true).queue(); + + return; + } + + event.getMessageChannel().retrieveMessageById(messageId).queue(message -> { + tagSystem.put(tagId, message.getContentRaw()); + + event + .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) + .setTitle(SUCCESS_LITERAL) + .setTimestamp(LocalDateTime.now()) + .setFooter(event.getUser().getAsTag()) + .setDescription("Successfully created tag '" + tagId + "'") + .build()) + .queue(); + }, failure -> { + if (failure instanceof ErrorResponseException ex + && ex.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) { + event.reply("This message doesn't exist").setEphemeral(true).queue(); + } else { + log.error("An unknown exception occurred", failure); + } + }); + } + + @Override + public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { + String userId = args.get(0); + + if (!event.getUser().getId().equals(userId) + && !event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { + event.reply(":police_car: Button theft is not allowed").setEphemeral(true).queue(); + + return; + } else if (event.getButton().getLabel().equals("Abort")) { + event.getMessage().delete().queue(); + + return; + } else if (!event.getButton().getLabel().equals("Of course!") + || event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { + return; + } + + String tagId = args.get(1); + + tagSystem.delete(tagId); + + event.getMessage().delete().queue(); + + event + .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) + .setTitle(SUCCESS_LITERAL) + .setTimestamp(LocalDateTime.now()) + .setFooter(event.getUser().getAsTag()) + .setDescription("Successfully deleted tag '" + tagId + "'") + .build()) + .setEphemeral(true) + .queue(); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java new file mode 100644 index 0000000000..3c63a383b9 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java @@ -0,0 +1,110 @@ +package org.togetherjava.tjbot.commands.tag; + +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.util.*; +import java.util.stream.Collectors; + +/** + * Tag system database utility.
+ * Has methods to store & retrieve tags from the database. + * + * @author illuminator3 + */ +public final class TagSystem { + private final Database database; + + public TagSystem(Database database) { + this.database = database; + } + + /** + * Checks if a tag already exists in the database + * + * @param tag id tag to check + * @return true if it exists, false if not + * @author illuminator3 + */ + public boolean exists(String tag) { + return database.readTransaction( + ctx -> ctx.selectFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(tag)).fetchOne() != null); + } + + /** + * Deletes a tag from the database + * + * @param tag tag to delete + * @author illuminator3 + */ + public void delete(String tag) { + database.writeTransaction(ctx -> { + ctx.deleteFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(tag)).execute(); + }); + } + + /** + * Inserts/updates a (new) tag into the database + * + * @param tag tag id + * @param content content of the tag + * @author illuminator3 + */ + public void put(String tag, String content) { + database.writeTransaction(ctx -> { + ctx.insertInto(Tags.TAGS, Tags.TAGS.ID, Tags.TAGS.CONTENT) + .values(tag, content) + .onDuplicateKeyUpdate() + .set(Tags.TAGS.CONTENT, content) + .execute(); + }); + } + + /** + * Retrieves the content of a tag from the database.
+ * + * @param tag tag id + * @return content of the tag, empty optional if the tag doesn't exist + * @author illuminator3 + */ + public Optional get(String tag) { + return database.readTransaction(ctx -> { + return Optional + .ofNullable(ctx.selectFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(tag)).fetchOne()) + .map(TagsRecord::getContent); + }); + } + + /** + * Retrieves all tags from the database + * + * @return all tags (id -> content) + * @author illuminator3 + */ + public Map retrieve() { + return database.readTransaction(ctx -> { + return ctx.selectFrom(Tags.TAGS) + .fetch() + .stream() + .collect(Collectors.toMap(TagsRecord::getId, TagsRecord::getContent)); + }); + } + + /** + * Retrieves all tag ids from the database.
+ * This method is defined like so: + * + *
+     * retrieveIds() {
+     *     return Collections.unmodifiableSet(retrieve().keySet());
+     * }
+     * 
+ * + * @return ids of all tags; unmodifiable set + * @author illuminator3 + */ + public Set retrieveIds() { + return Collections.unmodifiableSet(retrieve().keySet()); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java new file mode 100644 index 0000000000..77759ced01 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java @@ -0,0 +1,121 @@ +package org.togetherjava.tjbot.commands.tag; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; +import org.togetherjava.tjbot.commands.utils.MessageUtils; + +import java.awt.*; +import java.time.LocalDateTime; + +/** + * Utility class for the command system.
+ * Available methods:
+ * - {@link #generateEmbed(String, String)}
+ * - {@link #sendTag(MessageChannel, String, String, TagSystem, boolean, String)}
+ * - {@link #replyTag(SlashCommandEvent, String, String, TagSystem, boolean, String)}
+ * - {@link #buildAllTagsEmbed(String, TagSystem)} + * + * @author illuminator3 + */ +public final class TagUtility { + private TagUtility() {} + + /** + * Generates an embed with the given content. + * + * @param content content + * @param requestor user that requested the embed + * @return the generated embed + * @author illuminator3 + */ + public static MessageEmbed generateEmbed(String content, String requestor) { + return new EmbedBuilder().setDescription(content) + .setTimestamp(LocalDateTime.now()) + .setFooter(requestor) + .setColor(new Color(content.hashCode())) + .build(); + } + + /** + * Sends a tag into a given channel. + * + * @param channel channel the tag was requested in + * @param tagId tag id + * @param requestor user that requested the tag + * @param tagSystem current tag system instance + * @param isRaw if the tag should be displayed raw + * @param componentId generated componentId based on the user id + * @throws IllegalArgumentException if the tag does not exist + * @author illuminator3 + */ + public static void sendTag(MessageChannel channel, String tagId, String requestor, + TagSystem tagSystem, boolean isRaw, String componentId) + throws IllegalArgumentException { + String content = tagSystem.get(tagId) + .orElseThrow(() -> new IllegalArgumentException( + String.format("Tag '%s' doesn't exist", tagId))); + + channel + .sendMessageEmbeds(TagUtility.generateEmbed( + isRaw ? MessageUtils.escapeDiscordMessage(content) : content, requestor)) + .setActionRow(Button.of(ButtonStyle.DANGER, componentId, "Delete", + Emoji.fromUnicode("\uD83D\uDDD1"))) + .queue(); + } + + /** + * Replies to a message with a given tag. + * + * @param event slash command event causing this tag request + * @param tagId tag id + * @param requestor user that requested the tag + * @param tagSystem current tag system instance + * @param isRaw if the tag should be displayed raw + * @param componentId generated componentId based on the user id + * @throws IllegalArgumentException if the tag does not exist + * @author illuminator3 + */ + public static void replyTag(SlashCommandEvent event, String tagId, String requestor, + TagSystem tagSystem, boolean isRaw, String componentId) + throws IllegalArgumentException { + if (tagSystem.exists(tagId)) { + String content = tagSystem.get(tagId) + .orElseThrow(() -> new IllegalArgumentException( + String.format("Tag '%s' doesn't exist", tagId))); + + event + .replyEmbeds(TagUtility.generateEmbed( + isRaw ? MessageUtils.escapeDiscordMessage(content) : content, requestor)) + .addActionRow(Button.of(ButtonStyle.DANGER, componentId, "Delete", + Emoji.fromUnicode("\uD83D\uDDD1"))) + .queue(); + } else { + event + .replyEmbeds(buildAllTagsEmbed(requestor, tagSystem) + .setTitle("Could not find tag '" + tagId + "'") + .build()) + .setEphemeral(true) + .queue(); + } + } + + /** + * Builds an embed with all available tag ids as its description + * + * @param user user that requested the embed + * @param tagSystem current tag system instance + * @return the generated embed + * @author illuminator3 + */ + public static EmbedBuilder buildAllTagsEmbed(String user, TagSystem tagSystem) { + return new EmbedBuilder().setColor(Color.MAGENTA) + .setTimestamp(LocalDateTime.now()) + .setFooter(user) + .setDescription(String.join(", ", tagSystem.retrieveIds())); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java new file mode 100644 index 0000000000..ce8f8ed282 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java @@ -0,0 +1,52 @@ +package org.togetherjava.tjbot.commands.tag; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +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.commands.SlashCommandAdapter; +import org.togetherjava.tjbot.commands.SlashCommandVisibility; + +import java.util.List; + +/** + * Tags command. Usage: {@code /tags}. Displays all available tags. + * + * @author illuminator3 + */ +public final class TagsCommand extends SlashCommandAdapter { + private final TagSystem tagSystem; + + public TagsCommand(TagSystem tagSystem) { + super("tags", "Displays all tags", SlashCommandVisibility.GUILD); + + this.tagSystem = tagSystem; + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + event + .replyEmbeds(TagUtility.buildAllTagsEmbed(event.getUser().getAsTag(), tagSystem) + .setTitle("All available tags") + .build()) + .addActionRow( + Button.of(ButtonStyle.DANGER, generateComponentId(event.getUser().getId()), + "Delete", Emoji.fromUnicode("\uD83D\uDDD1"))) + .queue(); + } + + @Override + public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { + String userId = args.get(0); + + if (event.getUser().getId().equals(userId) + || event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { + event.getMessage().delete().queue(); + } else { + event.reply(":police_car: Button theft is not allowed").setEphemeral(true).queue(); + } + } +} 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..badb66e98f 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 @@ -6,6 +6,7 @@ import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.regex.Pattern; /** * Utility methods for {@link Message}. @@ -15,6 +16,8 @@ */ public class MessageUtils { + private static final Pattern ESCAPE_DISCORD_CHARACTERS = Pattern.compile("([^a-zA-Z0-9 \n\r])"); + private MessageUtils() { throw new UnsupportedOperationException(); } @@ -39,4 +42,30 @@ public static void disableButtons(@NotNull Message message) { .queue(); } + /** + * Escapes all characters that have a special meaning in Discord. + *

+ * Affected characters are everything that is neither {@code a-zA-Z0-9}, a {@code space}, + * {@code \n} or {@code \r}. Escaping is done by prefixing the character with a single backslash + * {@code \}. + *

+ * Example: + * + *

+     * {@code
+     * // Before
+     * `System.out.println("Hello World")`
+     * // After
+     * \`System\.out\.println\(\"Hello World\"\)\`
+     * }
+     * 
+ * + * @param message message to escape + * @return escaped message + * @author illuminator3 + */ + public static String escapeDiscordMessage(@NotNull CharSequence message) { + return ESCAPE_DISCORD_CHARACTERS.matcher(message).replaceAll("\\\\$1"); + } + } 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/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/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' - From 023f351b060b1a648f142381f3a22bd08623cd9c Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 14 Oct 2021 10:52:32 +0200 Subject: [PATCH 02/12] Removed "@author" tags --- .../tjbot/commands/tag/TagCommand.java | 2 -- .../tjbot/commands/tag/TagManageCommand.java | 2 -- .../tjbot/commands/tag/TagSystem.java | 8 ------- .../tjbot/commands/tag/TagUtility.java | 6 ----- .../tjbot/commands/tag/TagsCommand.java | 2 -- .../tjbot/commands/utils/MessageUtils.java | 1 - .../tjbot/formatter/CodeSectionFormatter.java | 23 ------------------- .../tjbot/formatter/Formatter.java | 13 ----------- .../tjbot/formatter/tokenizer/Lexer.java | 6 ----- .../tjbot/formatter/tokenizer/Token.java | 3 --- .../tjbot/formatter/tokenizer/TokenType.java | 2 -- .../tokenizer/TokenizationException.java | 2 -- .../formatter/util/LookaheadArrayDeque.java | 2 -- .../tjbot/formatter/util/LookaheadQueue.java | 2 -- .../tjbot/formatter/FormatterTest.java | 3 --- .../tjbot/formatter/tokenizer/LexerTest.java | 3 --- 16 files changed, 80 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java index 077ca5f9fd..11058aaecf 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java @@ -23,8 +23,6 @@ * {@code /tag}
* {@code /tag ask}
* {@code /tag ask true} - * - * @author illuminator3 */ public final class TagCommand extends SlashCommandAdapter { private final TagSystem tagSystem; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java index fd56089d51..a02768fc91 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java @@ -24,8 +24,6 @@ /** * TagManage command. Usage: * {@code /tagmanage [rawtag|edittag|editidtag|deletedtag|createtag|createidtag] (...)}. - * - * @author illuminator3 */ public final class TagManageCommand extends SlashCommandAdapter { private static final Logger log = LoggerFactory.getLogger(TagManageCommand.class); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java index 3c63a383b9..f64d60a3ca 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java @@ -10,8 +10,6 @@ /** * Tag system database utility.
* Has methods to store & retrieve tags from the database. - * - * @author illuminator3 */ public final class TagSystem { private final Database database; @@ -25,7 +23,6 @@ public TagSystem(Database database) { * * @param tag id tag to check * @return true if it exists, false if not - * @author illuminator3 */ public boolean exists(String tag) { return database.readTransaction( @@ -36,7 +33,6 @@ public boolean exists(String tag) { * Deletes a tag from the database * * @param tag tag to delete - * @author illuminator3 */ public void delete(String tag) { database.writeTransaction(ctx -> { @@ -49,7 +45,6 @@ public void delete(String tag) { * * @param tag tag id * @param content content of the tag - * @author illuminator3 */ public void put(String tag, String content) { database.writeTransaction(ctx -> { @@ -66,7 +61,6 @@ public void put(String tag, String content) { * * @param tag tag id * @return content of the tag, empty optional if the tag doesn't exist - * @author illuminator3 */ public Optional get(String tag) { return database.readTransaction(ctx -> { @@ -80,7 +74,6 @@ public Optional get(String tag) { * Retrieves all tags from the database * * @return all tags (id -> content) - * @author illuminator3 */ public Map retrieve() { return database.readTransaction(ctx -> { @@ -102,7 +95,6 @@ public Map retrieve() { * * * @return ids of all tags; unmodifiable set - * @author illuminator3 */ public Set retrieveIds() { return Collections.unmodifiableSet(retrieve().keySet()); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java index 77759ced01..28d7d19b3f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java @@ -19,8 +19,6 @@ * - {@link #sendTag(MessageChannel, String, String, TagSystem, boolean, String)}
* - {@link #replyTag(SlashCommandEvent, String, String, TagSystem, boolean, String)}
* - {@link #buildAllTagsEmbed(String, TagSystem)} - * - * @author illuminator3 */ public final class TagUtility { private TagUtility() {} @@ -31,7 +29,6 @@ private TagUtility() {} * @param content content * @param requestor user that requested the embed * @return the generated embed - * @author illuminator3 */ public static MessageEmbed generateEmbed(String content, String requestor) { return new EmbedBuilder().setDescription(content) @@ -51,7 +48,6 @@ public static MessageEmbed generateEmbed(String content, String requestor) { * @param isRaw if the tag should be displayed raw * @param componentId generated componentId based on the user id * @throws IllegalArgumentException if the tag does not exist - * @author illuminator3 */ public static void sendTag(MessageChannel channel, String tagId, String requestor, TagSystem tagSystem, boolean isRaw, String componentId) @@ -78,7 +74,6 @@ public static void sendTag(MessageChannel channel, String tagId, String requesto * @param isRaw if the tag should be displayed raw * @param componentId generated componentId based on the user id * @throws IllegalArgumentException if the tag does not exist - * @author illuminator3 */ public static void replyTag(SlashCommandEvent event, String tagId, String requestor, TagSystem tagSystem, boolean isRaw, String componentId) @@ -110,7 +105,6 @@ public static void replyTag(SlashCommandEvent event, String tagId, String reques * @param user user that requested the embed * @param tagSystem current tag system instance * @return the generated embed - * @author illuminator3 */ public static EmbedBuilder buildAllTagsEmbed(String user, TagSystem tagSystem) { return new EmbedBuilder().setColor(Color.MAGENTA) diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java index ce8f8ed282..87a8b036fb 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java @@ -14,8 +14,6 @@ /** * Tags command. Usage: {@code /tags}. Displays all available tags. - * - * @author illuminator3 */ public final class TagsCommand extends SlashCommandAdapter { private final TagSystem tagSystem; 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 badb66e98f..6612120b31 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 @@ -62,7 +62,6 @@ public static void disableButtons(@NotNull Message message) { * * @param message message to escape * @return escaped message - * @author illuminator3 */ public static String escapeDiscordMessage(@NotNull CharSequence message) { return ESCAPE_DISCORD_CHARACTERS.matcher(message).replaceAll("\\\\$1"); 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; From 0109911031f3c439334c8772dce87e1e22513276 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 14 Oct 2021 10:54:15 +0200 Subject: [PATCH 03/12] enum-utility-class pattern for `MessageUtils`, javadoc fix --- .../togetherjava/tjbot/commands/utils/MessageUtils.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 6612120b31..7ac3bbcdc8 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 @@ -14,18 +14,15 @@ * 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 { +public enum MessageUtils { + ; private static final Pattern ESCAPE_DISCORD_CHARACTERS = Pattern.compile("([^a-zA-Z0-9 \n\r])"); - private MessageUtils() { - throw new UnsupportedOperationException(); - } - /** * 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 From 285f265fed2617c5e27d091e6275280a590d9da0 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Thu, 14 Oct 2021 16:05:47 +0200 Subject: [PATCH 04/12] Polished tag system and architecture, removed incomplete features --- .../tjbot/commands/tag/TagCommand.java | 96 ++--- .../tjbot/commands/tag/TagContentStyle.java | 18 + .../tjbot/commands/tag/TagManageCommand.java | 394 ++++++++++-------- .../tjbot/commands/tag/TagSystem.java | 152 ++++--- .../tjbot/commands/tag/TagUtility.java | 115 ----- .../tjbot/commands/tag/TagsCommand.java | 48 ++- .../tjbot/commands/utils/MessageUtils.java | 28 +- 7 files changed, 432 insertions(+), 419 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tag/TagContentStyle.java delete mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java index 11058aaecf..3b8bd3747b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java @@ -1,100 +1,52 @@ package org.togetherjava.tjbot.commands.tag; -import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.Emoji; -import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; -import net.dv8tion.jda.api.events.interaction.SelectionMenuEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.components.Button; -import net.dv8tion.jda.api.interactions.components.ButtonStyle; -import net.dv8tion.jda.api.interactions.components.selections.SelectOption; -import net.dv8tion.jda.api.interactions.components.selections.SelectionMenu; 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; /** - * Tag command. Usage: {@code /tag (id) (raw)}
- * Example disord usages:
- * {@code /tag}
- * {@code /tag ask}
- * {@code /tag ask true} + * 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", "View a tag", SlashCommandVisibility.GUILD); + super("tag", "Display a tags content", SlashCommandVisibility.GUILD); this.tagSystem = tagSystem; - getData().addOption(OptionType.STRING, "id", "Tag id") - .addOption(OptionType.BOOLEAN, "raw", "Raw"); + // 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) { - OptionMapping idOption = event.getOption("id"); - - if (idOption != null) { - String tagId = idOption.getAsString(); - - TagUtility.replyTag(event, tagId, event.getUser().getAsTag(), tagSystem, - event.getOption("raw") != null, event.getUser().getId()); - + String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); + if (tagSystem.isUnknownTagAndHandle(id, event)) { return; } - SelectionMenu.Builder menu = - SelectionMenu.create(generateComponentId(event.getUser().getId())) - .setRequiredRange(1, 1); - - tagSystem.retrieveIds().stream().limit(25).forEach(tag -> menu.addOption(tag, tag)); - - event.reply("Choose a tag") - .addActionRow(menu.build()) - .addActionRow( - Button.of(ButtonStyle.DANGER, generateComponentId(event.getUser().getId()), - "Delete", Emoji.fromUnicode("\uD83D\uDDD1"))) + event + .replyEmbeds(MessageUtils.generateEmbed(null, tagSystem.getTag(id).orElseThrow(), + event.getUser(), TagSystem.AMBIENT_COLOR)) .queue(); } - - @Override - public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { - String userId = args.get(0); - - if (!event.getUser().getId().equals(userId) - && !event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { - event.reply(":police_car: Button theft is not allowed").setEphemeral(true).queue(); - - return; - } - - event.getMessage().delete().queue(); - } - - @Override - public void onSelectionMenu(@NotNull SelectionMenuEvent event, @NotNull List args) { - String userId = args.get(0); - - if (!event.getUser().getId().equals(userId)) { - event.reply(":police_car: Selection menu theft is not allowed") - .setEphemeral(true) - .queue(); - - return; - } - - SelectOption option = event.getSelectedOptions().get(0); - String tagId = option.getLabel(); - - event.getMessage().delete().queue(); - - TagUtility.sendTag(event.getMessageChannel(), tagId, event.getUser().getAsTag(), tagSystem, - false, generateComponentId(userId)); - } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagContentStyle.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagContentStyle.java new file mode 100644 index 0000000000..f2f84a7cc7 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagContentStyle.java @@ -0,0 +1,18 @@ +package org.togetherjava.tjbot.commands.tag; + +/** + * 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/tag/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java index a02768fc91..c2d8d099e0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java @@ -1,243 +1,309 @@ package org.togetherjava.tjbot.commands.tag; -import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; 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.interactions.components.Button; -import net.dv8tion.jda.api.interactions.components.ButtonStyle; 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.awt.*; -import java.time.LocalDateTime; -import java.util.List; +import java.util.Objects; +import java.util.OptionalLong; +import java.util.function.BiConsumer; +import java.util.function.Consumer; /** - * TagManage command. Usage: - * {@code /tagmanage [rawtag|edittag|editidtag|deletedtag|createtag|createidtag] (...)}. + * Implements the {@code /tag-manage} command which allows management of tags, such as creating, + * editing or deleting them. Available subcommands are: + *

    + *
  • {@code raw}
  • + *
  • {@code create}
  • + *
  • {@code create-with-message}
  • + *
  • {@code edit}
  • + *
  • {@code edit-with-message}
  • + *
  • {@code delete}
  • + *
+ *

+ * 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 log = LoggerFactory.getLogger(TagManageCommand.class); - - private static final String TAG_ID_LITERAL = "tag-id"; - private static final String CONTENT_LITERAL = "content"; - private static final String MESSAGE_ID_LITERAL = "message-id"; - private static final String SUCCESS_LITERAL = "Success"; - private static final String TAG_DOES_NOT_EXIST = "This tag does not exist"; - private static final String TAG_ALREADY_EXISTS = "This tag already exists"; - + 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("tagmanage", "Manage the tags", SlashCommandVisibility.GUILD); + super("tag-manage", "Provides commands to manage all tags", SlashCommandVisibility.GUILD); this.tagSystem = tagSystem; - getData().addSubcommands( - new SubcommandData("rawtag", "View a tag in its raw form") - .addOption(OptionType.STRING, "id", TAG_ID_LITERAL), - new SubcommandData("edittag", "Edit a tag") - .addOption(OptionType.STRING, "id", TAG_ID_LITERAL, true) - .addOption(OptionType.STRING, CONTENT_LITERAL, "Tag content", true), - new SubcommandData("editidtag", "Edit a tag using a message id") - .addOption(OptionType.STRING, TAG_ID_LITERAL, TAG_ID_LITERAL, true) - .addOption(OptionType.NUMBER, MESSAGE_ID_LITERAL, MESSAGE_ID_LITERAL, true), - new SubcommandData("deletetag", "Delete a tag").addOption(OptionType.STRING, "id", - TAG_ID_LITERAL, true), - new SubcommandData("createtag", "Create a tag") - .addOption(OptionType.STRING, "id", TAG_ID_LITERAL, true) - .addOption(OptionType.STRING, CONTENT_LITERAL, "Tag content", true), - new SubcommandData("createidtag", "Create a tag using a message id") - .addOption(OptionType.STRING, TAG_ID_LITERAL, TAG_ID_LITERAL, true) - .addOption(OptionType.NUMBER, MESSAGE_ID_LITERAL, MESSAGE_ID_LITERAL, true)); + // 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) { + 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 = event.getMember(); + Member member = Objects.requireNonNull(event.getMember()); if (!member.hasPermission(Permission.MESSAGE_MANAGE)) { - event.reply("You need the MESSAGE_MANAGE permission to use this command!") + event.reply( + "Tags can only be managed by users who have the 'MESSAGE_MANAGE' permission.") .setEphemeral(true) .queue(); - return; } - switch (event.getSubcommandName()) { - case "edittag" -> editTag(event); - case "editidtag" -> editIdTag(event); - case "deletetag" -> deleteTag(event, member); - case "createtag" -> createTag(event); - case "createidtag" -> createIdTag(event); - default -> throw new IllegalStateException(); + 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 editTag(@NotNull SlashCommandEvent event) { - String tagId = event.getOption("id").getAsString(); - String content = event.getOption(CONTENT_LITERAL).getAsString(); - - if (!tagSystem.exists(tagId)) { - event.reply(TAG_DOES_NOT_EXIST).setEphemeral(true).queue(); - + private void rawTag(@NotNull SlashCommandEvent event) { + String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); + if (tagSystem.isUnknownTagAndHandle(id, event)) { return; } - tagSystem.put(tagId, content); - - event - .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) - .setTitle(SUCCESS_LITERAL) - .setTimestamp(LocalDateTime.now()) - .setFooter(event.getUser().getAsTag()) - .setDescription("Successfully edited tag '" + tagId + "'") - .build()) + event.replyEmbeds(MessageUtils.generateEmbed(null, + MessageUtils.escapeDiscordMessage(tagSystem.getTag(id).orElseThrow()), + event.getUser(), TagSystem.AMBIENT_COLOR)) .queue(); } - private void editIdTag(@NotNull SlashCommandEvent event) { - String tagId = event.getOption(TAG_ID_LITERAL).getAsString(); - long messageId = event.getOption(MESSAGE_ID_LITERAL).getAsLong(); + private void createTag(@NotNull CommandInteraction event) { + String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString(); - if (!tagSystem.exists(tagId)) { - event.reply(TAG_DOES_NOT_EXIST).setEphemeral(true).queue(); + handleAction(TagStatus.NOT_EXISTS, id -> tagSystem.putTag(id, content), "created", event); + } - return; - } + private void createTagWithMessage(@NotNull CommandInteraction event) { + handleActionWithMessage(TagStatus.NOT_EXISTS, tagSystem::putTag, "created", event); + } - event.getMessageChannel().retrieveMessageById(messageId).queue(message -> { - tagSystem.put(tagId, message.getContentRaw()); + private void editTag(@NotNull CommandInteraction event) { + String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString(); - event - .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) - .setTitle(SUCCESS_LITERAL) - .setTimestamp(LocalDateTime.now()) - .setFooter(event.getUser().getAsTag()) - .setDescription("Successfully edited tag '" + tagId + "'") - .build()) - .queue(); - }, failure -> { - if (failure instanceof ErrorResponseException ex - && ex.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) { - event.reply("This message doesn't exist").setEphemeral(true).queue(); - } else { - log.error("An unknown exception occurred", failure); - } - }); + handleAction(TagStatus.EXISTS, id -> tagSystem.putTag(id, content), "edited", event); } - private void deleteTag(@NotNull SlashCommandEvent event, @NotNull Member member) { - String tagId = event.getOption("id").getAsString(); + private void editTagWithMessage(@NotNull CommandInteraction event) { + handleActionWithMessage(TagStatus.EXISTS, tagSystem::putTag, "edited", event); + } - if (!tagSystem.exists(tagId)) { - event.reply(TAG_DOES_NOT_EXIST).setEphemeral(true).queue(); + 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; } - event.reply("You sure? Confirming this will delete the tag '" + tagId + "'") - .addActionRow( - Button.of(ButtonStyle.DANGER, generateComponentId(member.getId(), tagId), - "Of course!"), - Button.of(ButtonStyle.SUCCESS, generateComponentId(member.getId()), "Abort")) - .queue(); + idAction.accept(id); + sendSuccessMessage(event, id, actionVerb); } - private void createTag(@NotNull SlashCommandEvent event) { - String tagId = event.getOption("id").getAsString(); - String content = event.getOption(CONTENT_LITERAL).getAsString(); - - if (tagSystem.exists(tagId)) { - event.reply(TAG_ALREADY_EXISTS).setEphemeral(true).queue(); - + /** + * 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; } - - tagSystem.put(tagId, content); - - event - .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) - .setTitle(SUCCESS_LITERAL) - .setTimestamp(LocalDateTime.now()) - .setFooter(event.getUser().getAsTag()) - .setDescription("Successfully created tag '" + tagId + "'") - .build()) - .queue(); - } - - private void createIdTag(@NotNull SlashCommandEvent event) { - String tagId = event.getOption(TAG_ID_LITERAL).getAsString(); - long messageId = event.getOption(MESSAGE_ID_LITERAL).getAsLong(); - - if (tagSystem.exists(tagId)) { - event.reply(TAG_ALREADY_EXISTS).setEphemeral(true).queue(); - + long messageId = messageIdOpt.orElseThrow(); + if (isWrongTagStatusAndHandle(requiredTagStatus, tagId, event)) { return; } event.getMessageChannel().retrieveMessageById(messageId).queue(message -> { - tagSystem.put(tagId, message.getContentRaw()); - - event - .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) - .setTitle(SUCCESS_LITERAL) - .setTimestamp(LocalDateTime.now()) - .setFooter(event.getUser().getAsTag()) - .setDescription("Successfully created tag '" + tagId + "'") - .build()) - .queue(); + idAndContentAction.accept(tagId, message.getContentRaw()); + sendSuccessMessage(event, tagId, actionVerb); }, failure -> { if (failure instanceof ErrorResponseException ex && ex.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) { - event.reply("This message doesn't exist").setEphemeral(true).queue(); - } else { - log.error("An unknown exception occurred", failure); + 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(); }); } - @Override - public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { - String userId = args.get(0); + /** + * 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; + } - if (!event.getUser().getId().equals(userId) - && !event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { - event.reply(":police_car: Button theft is not allowed").setEphemeral(true).queue(); + private enum TagStatus { + EXISTS, + NOT_EXISTS + } - return; - } else if (event.getButton().getLabel().equals("Abort")) { - event.getMessage().delete().queue(); - return; - } else if (!event.getButton().getLabel().equals("Of course!") - || event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { - return; - } + private enum Subcommand { + RAW("raw"), + CREATE("create"), + CREATE_WITH_MESSAGE("create-with-message"), + EDIT("edit"), + EDIT_WITH_MESSAGE("edit-with-message"), + DELETE("delete"); - String tagId = args.get(1); + private final String name; - tagSystem.delete(tagId); - - event.getMessage().delete().queue(); + Subcommand(String name) { + this.name = name; + } - event - .replyEmbeds(new EmbedBuilder().setColor(Color.GREEN) - .setTitle(SUCCESS_LITERAL) - .setTimestamp(LocalDateTime.now()) - .setFooter(event.getUser().getAsTag()) - .setDescription("Successfully deleted tag '" + tagId + "'") - .build()) - .setEphemeral(true) - .queue(); + public 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/tag/TagSystem.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java index f64d60a3ca..7025c78040 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java @@ -1,102 +1,152 @@ package org.togetherjava.tjbot.commands.tag; +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.util.*; +import java.awt.*; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; /** - * Tag system database utility.
- * Has methods to store & retrieve tags from the database. + * 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; } /** - * Checks if a tag already exists in the database + * Creates a delete button with the given component id that can be used in message dialogs. For + * example to delete a message. * - * @param tag id tag to check - * @return true if it exists, false if not + * @param componentId the component id to use for the button + * @return the created delete button */ - public boolean exists(String tag) { - return database.readTransaction( - ctx -> ctx.selectFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(tag)).fetchOne() != null); + static Button createDeleteButton(String componentId) { + return Button.of(ButtonStyle.DANGER, componentId, "Delete", + Emoji.fromUnicode("\uD83D\uDDD1")); // trash bin } /** - * Deletes a tag from the database + * Returns whether the given tag is unknown to the system. + *

+ * If it is unknown, it sends an error message to the user. * - * @param tag tag to delete + * @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 */ - public void delete(String tag) { - database.writeTransaction(ctx -> { - ctx.deleteFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(tag)).execute(); - }); + boolean isUnknownTagAndHandle(@NotNull String id, @NotNull Interaction event) { + if (hasTag(id)) { + return false; + } + // TODO Add fuzzy string matching suggestions (merge with TagCommand who uses this as well) + event.reply("Could not find any tag with id '%s'.".formatted(id)) + .setEphemeral(true) + .queue(); + return true; } /** - * Inserts/updates a (new) tag into the database + * Checks if the given tag is known to the tag system. * - * @param tag tag id - * @param content content of the tag + * @param id the id of the tag to check + * @return whether the tag is known to the tag system */ - public void put(String tag, String content) { - database.writeTransaction(ctx -> { - ctx.insertInto(Tags.TAGS, Tags.TAGS.ID, Tags.TAGS.CONTENT) - .values(tag, content) - .onDuplicateKeyUpdate() - .set(Tags.TAGS.CONTENT, content) - .execute(); + 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; + } }); } /** - * Retrieves the content of a tag from the database.
+ * Deletes a tag from the tag system. * - * @param tag tag id - * @return content of the tag, empty optional if the tag doesn't exist + * @param id the id of the tag to delete + * @throws IllegalArgumentException if the tag is unknown to the system, see + * {@link #hasTag(String)} */ - public Optional get(String tag) { - return database.readTransaction(ctx -> { - return Optional - .ofNullable(ctx.selectFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(tag)).fetchOne()) - .map(TagsRecord::getContent); + void deleteTag(String id) { + int deletedRecords = database.write(context -> { + try (var deleteFrom = context.deleteFrom(Tags.TAGS)) { + return deleteFrom.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)); + } } /** - * Retrieves all tags from the database + * Inserts or replaces the tag with the given data into the system. * - * @return all tags (id -> content) + * @param id the id of the tag to put + * @param content the content of the tag to put */ - public Map retrieve() { - return database.readTransaction(ctx -> { - return ctx.selectFrom(Tags.TAGS) - .fetch() - .stream() - .collect(Collectors.toMap(TagsRecord::getId, TagsRecord::getContent)); + void putTag(String id, String content) { + database.writeTransaction(context -> { + try (var insertInto = context.insertInto(Tags.TAGS, Tags.TAGS.ID, Tags.TAGS.CONTENT); + var query = insertInto.values(id, content) + .onDuplicateKeyUpdate() + .set(Tags.TAGS.CONTENT, content)) { + query.execute(); + } }); } /** - * Retrieves all tag ids from the database.
- * This method is defined like so: + * Retrieves the content of the given tag, if it is known to the system (see + * {@link #hasTag(String)}). * - *

-     * retrieveIds() {
-     *     return Collections.unmodifiableSet(retrieve().keySet());
-     * }
-     * 
+ * @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 ids of all tags; unmodifiable set + * @return a set of all ids known to the system, not backed */ - public Set retrieveIds() { - return Collections.unmodifiableSet(retrieve().keySet()); + 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/tag/TagUtility.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java deleted file mode 100644 index 28d7d19b3f..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagUtility.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.togetherjava.tjbot.commands.tag; - -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.entities.Emoji; -import net.dv8tion.jda.api.entities.MessageChannel; -import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -import net.dv8tion.jda.api.interactions.components.Button; -import net.dv8tion.jda.api.interactions.components.ButtonStyle; -import org.togetherjava.tjbot.commands.utils.MessageUtils; - -import java.awt.*; -import java.time.LocalDateTime; - -/** - * Utility class for the command system.
- * Available methods:
- * - {@link #generateEmbed(String, String)}
- * - {@link #sendTag(MessageChannel, String, String, TagSystem, boolean, String)}
- * - {@link #replyTag(SlashCommandEvent, String, String, TagSystem, boolean, String)}
- * - {@link #buildAllTagsEmbed(String, TagSystem)} - */ -public final class TagUtility { - private TagUtility() {} - - /** - * Generates an embed with the given content. - * - * @param content content - * @param requestor user that requested the embed - * @return the generated embed - */ - public static MessageEmbed generateEmbed(String content, String requestor) { - return new EmbedBuilder().setDescription(content) - .setTimestamp(LocalDateTime.now()) - .setFooter(requestor) - .setColor(new Color(content.hashCode())) - .build(); - } - - /** - * Sends a tag into a given channel. - * - * @param channel channel the tag was requested in - * @param tagId tag id - * @param requestor user that requested the tag - * @param tagSystem current tag system instance - * @param isRaw if the tag should be displayed raw - * @param componentId generated componentId based on the user id - * @throws IllegalArgumentException if the tag does not exist - */ - public static void sendTag(MessageChannel channel, String tagId, String requestor, - TagSystem tagSystem, boolean isRaw, String componentId) - throws IllegalArgumentException { - String content = tagSystem.get(tagId) - .orElseThrow(() -> new IllegalArgumentException( - String.format("Tag '%s' doesn't exist", tagId))); - - channel - .sendMessageEmbeds(TagUtility.generateEmbed( - isRaw ? MessageUtils.escapeDiscordMessage(content) : content, requestor)) - .setActionRow(Button.of(ButtonStyle.DANGER, componentId, "Delete", - Emoji.fromUnicode("\uD83D\uDDD1"))) - .queue(); - } - - /** - * Replies to a message with a given tag. - * - * @param event slash command event causing this tag request - * @param tagId tag id - * @param requestor user that requested the tag - * @param tagSystem current tag system instance - * @param isRaw if the tag should be displayed raw - * @param componentId generated componentId based on the user id - * @throws IllegalArgumentException if the tag does not exist - */ - public static void replyTag(SlashCommandEvent event, String tagId, String requestor, - TagSystem tagSystem, boolean isRaw, String componentId) - throws IllegalArgumentException { - if (tagSystem.exists(tagId)) { - String content = tagSystem.get(tagId) - .orElseThrow(() -> new IllegalArgumentException( - String.format("Tag '%s' doesn't exist", tagId))); - - event - .replyEmbeds(TagUtility.generateEmbed( - isRaw ? MessageUtils.escapeDiscordMessage(content) : content, requestor)) - .addActionRow(Button.of(ButtonStyle.DANGER, componentId, "Delete", - Emoji.fromUnicode("\uD83D\uDDD1"))) - .queue(); - } else { - event - .replyEmbeds(buildAllTagsEmbed(requestor, tagSystem) - .setTitle("Could not find tag '" + tagId + "'") - .build()) - .setEphemeral(true) - .queue(); - } - } - - /** - * Builds an embed with all available tag ids as its description - * - * @param user user that requested the embed - * @param tagSystem current tag system instance - * @return the generated embed - */ - public static EmbedBuilder buildAllTagsEmbed(String user, TagSystem tagSystem) { - return new EmbedBuilder().setColor(Color.MAGENTA) - .setTimestamp(LocalDateTime.now()) - .setFooter(user) - .setDescription(String.join(", ", tagSystem.retrieveIds())); - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java index 87a8b036fb..33ea75988a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java @@ -1,38 +1,51 @@ package org.togetherjava.tjbot.commands.tag; import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.Emoji; import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -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.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; +import org.togetherjava.tjbot.commands.utils.MessageUtils; import java.util.List; +import java.util.Objects; /** - * Tags command. Usage: {@code /tags}. Displays all available tags. + * 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 tags", SlashCommandVisibility.GUILD); + super("tags", "Displays all available tags", SlashCommandVisibility.GUILD); this.tagSystem = tagSystem; } @Override public void onSlashCommand(@NotNull SlashCommandEvent event) { - event - .replyEmbeds(TagUtility.buildAllTagsEmbed(event.getUser().getAsTag(), tagSystem) - .setTitle("All available tags") - .build()) + // 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( - Button.of(ButtonStyle.DANGER, generateComponentId(event.getUser().getId()), - "Delete", Emoji.fromUnicode("\uD83D\uDDD1"))) + TagSystem.createDeleteButton(generateComponentId(event.getUser().getId()))) .queue(); } @@ -40,11 +53,14 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) { public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { String userId = args.get(0); - if (event.getUser().getId().equals(userId) - || event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { - event.getMessage().delete().queue(); - } else { - event.reply(":police_car: Button theft is not allowed").setEphemeral(true).queue(); + 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(); } + + event.getMessage().delete().queue(); } } 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 7ac3bbcdc8..6c0359a6ab 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,16 @@ 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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.awt.*; +import java.time.Instant; import java.util.List; import java.util.regex.Pattern; @@ -47,7 +53,7 @@ public static void disableButtons(@NotNull Message message) { * {@code \}. *

* Example: - * + * *

      * {@code
      * // Before
@@ -64,4 +70,24 @@ public static String escapeDiscordMessage(@NotNull CharSequence message) {
         return ESCAPE_DISCORD_CHARACTERS.matcher(message).replaceAll("\\\\$1");
     }
 
+    /**
+     * 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();
+    }
+
 }

From a9c3a399c003f71621f0e3e1d4b276381f17c1d8 Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Thu, 14 Oct 2021 16:06:50 +0200
Subject: [PATCH 05/12] Refactored, package "tag" -> "tags"

---
 .../java/org/togetherjava/tjbot/commands/Commands.java    | 8 ++++----
 .../tjbot/commands/{tag => tags}/TagCommand.java          | 2 +-
 .../tjbot/commands/{tag => tags}/TagContentStyle.java     | 2 +-
 .../tjbot/commands/{tag => tags}/TagManageCommand.java    | 2 +-
 .../tjbot/commands/{tag => tags}/TagSystem.java           | 2 +-
 .../tjbot/commands/{tag => tags}/TagsCommand.java         | 2 +-
 6 files changed, 9 insertions(+), 9 deletions(-)
 rename application/src/main/java/org/togetherjava/tjbot/commands/{tag => tags}/TagCommand.java (97%)
 rename application/src/main/java/org/togetherjava/tjbot/commands/{tag => tags}/TagContentStyle.java (91%)
 rename application/src/main/java/org/togetherjava/tjbot/commands/{tag => tags}/TagManageCommand.java (99%)
 rename application/src/main/java/org/togetherjava/tjbot/commands/{tag => tags}/TagSystem.java (99%)
 rename application/src/main/java/org/togetherjava/tjbot/commands/{tag => tags}/TagsCommand.java (98%)

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 09603ead9c..1da8a004cb 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java
@@ -4,10 +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.tag.TagCommand;
-import org.togetherjava.tjbot.commands.tag.TagManageCommand;
-import org.togetherjava.tjbot.commands.tag.TagSystem;
-import org.togetherjava.tjbot.commands.tag.TagsCommand;
+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;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java
similarity index 97%
rename from application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java
rename to application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java
index 3b8bd3747b..52993e0adc 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java
@@ -1,4 +1,4 @@
-package org.togetherjava.tjbot.commands.tag;
+package org.togetherjava.tjbot.commands.tags;
 
 import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
 import net.dv8tion.jda.api.interactions.commands.OptionType;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagContentStyle.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagContentStyle.java
similarity index 91%
rename from application/src/main/java/org/togetherjava/tjbot/commands/tag/TagContentStyle.java
rename to application/src/main/java/org/togetherjava/tjbot/commands/tags/TagContentStyle.java
index f2f84a7cc7..898576a394 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagContentStyle.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagContentStyle.java
@@ -1,4 +1,4 @@
-package org.togetherjava.tjbot.commands.tag;
+package org.togetherjava.tjbot.commands.tags;
 
 /**
  * The style of a tag content.
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
similarity index 99%
rename from application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java
rename to application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
index c2d8d099e0..4b0020fdf9 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagManageCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
@@ -1,4 +1,4 @@
-package org.togetherjava.tjbot.commands.tag;
+package org.togetherjava.tjbot.commands.tags;
 
 import net.dv8tion.jda.api.Permission;
 import net.dv8tion.jda.api.entities.Member;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
similarity index 99%
rename from application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java
rename to application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
index 7025c78040..5195c82c33 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagSystem.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
@@ -1,4 +1,4 @@
-package org.togetherjava.tjbot.commands.tag;
+package org.togetherjava.tjbot.commands.tags;
 
 import net.dv8tion.jda.api.entities.Emoji;
 import net.dv8tion.jda.api.interactions.Interaction;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java
similarity index 98%
rename from application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java
rename to application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java
index 33ea75988a..d4f4b86d42 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tag/TagsCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java
@@ -1,4 +1,4 @@
-package org.togetherjava.tjbot.commands.tag;
+package org.togetherjava.tjbot.commands.tags;
 
 import net.dv8tion.jda.api.Permission;
 import net.dv8tion.jda.api.events.interaction.ButtonClickEvent;

From 437745b11986dc9bb6b73f156ca1cbac548bc11d Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Thu, 14 Oct 2021 16:10:40 +0200
Subject: [PATCH 06/12] bugfix with missing return leads to message deletion

---
 .../java/org/togetherjava/tjbot/commands/tags/TagsCommand.java   | 1 +
 1 file changed, 1 insertion(+)

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
index d4f4b86d42..82baa36a94 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagsCommand.java
@@ -59,6 +59,7 @@ public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List
                     "The message can only be deleted by its author or an user with 'MESSAGE_MANAGE' permissions.")
                 .setEphemeral(true)
                 .queue();
+            return;
         }
 
         event.getMessage().delete().queue();

From 7e7d423516491866f4e1d8ac211a01f09afac52e Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Thu, 14 Oct 2021 16:18:23 +0200
Subject: [PATCH 07/12] Added logging to tag-manage actions (INFO)

---
 .../java/org/togetherjava/tjbot/commands/tags/TagCommand.java    | 1 -
 .../org/togetherjava/tjbot/commands/tags/TagManageCommand.java   | 1 +
 2 files changed, 1 insertion(+), 1 deletion(-)

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
index 52993e0adc..e82b1ad827 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagCommand.java
@@ -33,7 +33,6 @@ public TagCommand(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);
     }
 
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
index 4b0020fdf9..bc04a7c4f5 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
@@ -83,6 +83,7 @@ public TagManageCommand(TagSystem tagSystem) {
 
     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))

From 74a96ec26470eee16771f063b330bb5fc4e1d1d6 Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Thu, 14 Oct 2021 16:20:52 +0200
Subject: [PATCH 08/12] Added missing package-info

---
 .../org/togetherjava/tjbot/commands/tags/package-info.java | 7 +++++++
 .../togetherjava/tjbot/commands/utils/package-info.java    | 4 ++++
 2 files changed, 11 insertions(+)
 create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/tags/package-info.java
 create mode 100644 application/src/main/java/org/togetherjava/tjbot/commands/utils/package-info.java

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

From bbf70d305b24dfec984a2751c6639e9f9146df26 Mon Sep 17 00:00:00 2001
From: Zabuzard 
Date: Thu, 14 Oct 2021 16:27:51 +0200
Subject: [PATCH 09/12] Fixed linter issues

---
 .../src/main/java/org/togetherjava/tjbot/Application.java  | 1 +
 .../java/org/togetherjava/tjbot/BootstrapLauncher.java     | 7 ++++++-
 .../java/org/togetherjava/tjbot/commands/SlashCommand.java | 6 +++---
 .../togetherjava/tjbot/commands/SlashCommandAdapter.java   | 2 +-
 .../togetherjava/tjbot/commands/system/ReloadCommand.java  | 4 +---
 .../togetherjava/tjbot/commands/tags/TagManageCommand.java | 2 +-
 .../org/togetherjava/tjbot/commands/tags/TagSystem.java    | 3 ++-
 .../main/java/org/togetherjava/tjbot/config/Config.java    | 1 +
 8 files changed, 16 insertions(+), 10 deletions(-)

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/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/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java index bc04a7c4f5..7b0e946029 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java @@ -297,7 +297,7 @@ private enum Subcommand { this.name = name; } - public static Subcommand fromName(String name) { + static Subcommand fromName(String name) { for (Subcommand subcommand : Subcommand.values()) { if (subcommand.name.equals(name)) { return subcommand; 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 index 5195c82c33..3eb7314f2d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java @@ -42,6 +42,7 @@ public TagSystem(Database database) { * @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 @@ -60,7 +61,7 @@ boolean isUnknownTagAndHandle(@NotNull String id, @NotNull Interaction event) { if (hasTag(id)) { return false; } - // TODO Add fuzzy string matching suggestions (merge with TagCommand who uses this as well) + // TODO Add fuzzy string matching suggestions (Levenshtein edit distance) event.reply("Could not find any tag with id '%s'.".formatted(id)) .setEphemeral(true) .queue(); 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; From 6c3158bda6e0acf1b2e73914b75af2735e38b2cf Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Sat, 16 Oct 2021 18:55:48 +0200 Subject: [PATCH 10/12] Removed try-with-resources (CR Istannen) --- .../tjbot/commands/tags/TagSystem.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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 index 3eb7314f2d..b2698ea52a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java @@ -89,11 +89,11 @@ boolean hasTag(String id) { * @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 -> { - try (var deleteFrom = context.deleteFrom(Tags.TAGS)) { - return deleteFrom.where(Tags.TAGS.ID.eq(id)).execute(); - } + return context.deleteFrom(Tags.TAGS).where(Tags.TAGS.ID.eq(id)).execute(); }); if (deletedRecords == 0) { throw new IllegalArgumentException( @@ -107,14 +107,15 @@ void deleteTag(String id) { * @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 -> { - try (var insertInto = context.insertInto(Tags.TAGS, Tags.TAGS.ID, Tags.TAGS.CONTENT); - var query = insertInto.values(id, content) - .onDuplicateKeyUpdate() - .set(Tags.TAGS.CONTENT, content)) { - query.execute(); - } + context.insertInto(Tags.TAGS, Tags.TAGS.ID, Tags.TAGS.CONTENT) + .values(id, content) + .onDuplicateKeyUpdate() + .set(Tags.TAGS.CONTENT, content) + .execute(); }); } From 904ffaafcca5a255927f6ad780186282d81579ac Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Tue, 19 Oct 2021 13:56:16 +0200 Subject: [PATCH 11/12] Replaced discord message escape by JDA utility Spotless after rebase --- .../togetherjava/tjbot/commands/Commands.java | 5 ++-- .../tjbot/commands/tags/TagManageCommand.java | 5 ++-- .../tjbot/commands/utils/MessageUtils.java | 28 ------------------- 3 files changed, 6 insertions(+), 32 deletions(-) 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 1da8a004cb..3961b19ed8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java @@ -39,7 +39,8 @@ public enum Commands { // 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(), new TagCommand(tagSystem), - new TagManageCommand(tagSystem), new TagsCommand(tagSystem)); + 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/tags/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java index 7b0e946029..66d4be77aa 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java @@ -9,6 +9,7 @@ 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 net.dv8tion.jda.api.utils.MarkdownSanitizer; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -145,8 +146,8 @@ private void rawTag(@NotNull SlashCommandEvent event) { } event.replyEmbeds(MessageUtils.generateEmbed(null, - MessageUtils.escapeDiscordMessage(tagSystem.getTag(id).orElseThrow()), - event.getUser(), TagSystem.AMBIENT_COLOR)) + MarkdownSanitizer.escape(tagSystem.getTag(id).orElseThrow()), event.getUser(), + TagSystem.AMBIENT_COLOR)) .queue(); } 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 6c0359a6ab..b7465f9ad3 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 @@ -12,7 +12,6 @@ import java.awt.*; import java.time.Instant; import java.util.List; -import java.util.regex.Pattern; /** * Utility methods for {@link Message}. @@ -23,8 +22,6 @@ public enum MessageUtils { ; - private static final Pattern ESCAPE_DISCORD_CHARACTERS = Pattern.compile("([^a-zA-Z0-9 \n\r])"); - /** * Disables all the buttons that a message has. Disabling buttons deems it as not clickable to * the user who sees it. @@ -45,31 +42,6 @@ public static void disableButtons(@NotNull Message message) { .queue(); } - /** - * Escapes all characters that have a special meaning in Discord. - *

- * Affected characters are everything that is neither {@code a-zA-Z0-9}, a {@code space}, - * {@code \n} or {@code \r}. Escaping is done by prefixing the character with a single backslash - * {@code \}. - *

- * Example: - * - *

-     * {@code
-     * // Before
-     * `System.out.println("Hello World")`
-     * // After
-     * \`System\.out\.println\(\"Hello World\"\)\`
-     * }
-     * 
- * - * @param message message to escape - * @return escaped message - */ - public static String escapeDiscordMessage(@NotNull CharSequence message) { - return ESCAPE_DISCORD_CHARACTERS.matcher(message).replaceAll("\\\\$1"); - } - /** * Generates an embed with the given content. * From 41568331ec6bb0833e82a55a5f7b162f6538135f Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Tue, 19 Oct 2021 14:35:33 +0200 Subject: [PATCH 12/12] Bugfix with broken markdown escaper (+test) * markdown escaper doesnt escape backslash \ correctly. * introduced own escaper layer to correct it * covered by unit tests Spotless after comment --- .../tjbot/commands/tags/TagManageCommand.java | 3 +- .../tjbot/commands/utils/MessageUtils.java | 15 +++++ .../commands/utils/MessageUtilsTest.java | 57 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 application/src/test/java/org/togetherjava/tjbot/commands/utils/MessageUtilsTest.java 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 index 66d4be77aa..cff16854ef 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java @@ -9,7 +9,6 @@ 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 net.dv8tion.jda.api.utils.MarkdownSanitizer; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -146,7 +145,7 @@ private void rawTag(@NotNull SlashCommandEvent event) { } event.replyEmbeds(MessageUtils.generateEmbed(null, - MarkdownSanitizer.escape(tagSystem.getTag(id).orElseThrow()), event.getUser(), + MessageUtils.escapeMarkdown(tagSystem.getTag(id).orElseThrow()), event.getUser(), TagSystem.AMBIENT_COLOR)) .queue(); } 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 b7465f9ad3..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 @@ -6,6 +6,7 @@ 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; @@ -62,4 +63,18 @@ public static void disableButtons(@NotNull Message message) { .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/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) { + } +}