diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java index 49105e33da..904c831439 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -19,6 +19,7 @@ import org.togetherjava.tjbot.commands.tophelper.TopHelpersPurgeMessagesRoutine; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.moderation.ModAuditLogWriter; import org.togetherjava.tjbot.routines.ModAuditLogRoutine; import java.util.ArrayList; @@ -50,6 +51,7 @@ public enum Features { @NotNull Database database, @NotNull Config config) { TagSystem tagSystem = new TagSystem(database); ModerationActionsStore actionsStore = new ModerationActionsStore(database); + ModAuditLogWriter modAuditLogWriter = new ModAuditLogWriter(config); // NOTE The system can add special system relevant commands also by itself, // hence this list may not necessarily represent the full list of all commands actually @@ -57,7 +59,7 @@ public enum Features { Collection features = new ArrayList<>(); // Routines - features.add(new ModAuditLogRoutine(database, config)); + features.add(new ModAuditLogRoutine(database, config, modAuditLogWriter)); features.add(new TemporaryModerationRoutine(jda, actionsStore, config)); features.add(new TopHelpersPurgeMessagesRoutine(database)); features.add(new RemindRoutine(database)); @@ -73,7 +75,7 @@ public enum Features { features.add(new PingCommand()); features.add(new TeXCommand()); features.add(new TagCommand(tagSystem)); - features.add(new TagManageCommand(tagSystem, config)); + features.add(new TagManageCommand(tagSystem, config, modAuditLogWriter)); features.add(new TagsCommand(tagSystem)); features.add(new VcActivityCommand()); features.add(new WarnCommand(actionsStore, config)); 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 16be87f43c..e2bae097b8 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 @@ -1,8 +1,10 @@ package org.togetherjava.tjbot.commands.tags; import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.exceptions.ErrorResponseException; import net.dv8tion.jda.api.interactions.Interaction; @@ -11,12 +13,17 @@ import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; import net.dv8tion.jda.api.requests.ErrorResponse; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.moderation.ModAuditLogWriter; +import java.time.temporal.TemporalAccessor; +import java.util.*; +import java.util.NoSuchElementException; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Objects; @@ -49,21 +56,37 @@ public final class TagManageCommand extends SlashCommandAdapter { private static final String CONTENT_DESCRIPTION = "the content of the tag"; static final String MESSAGE_ID_OPTION = "message-id"; private static final String MESSAGE_ID_DESCRIPTION = "the id of the message to refer to"; + + // "Edited tag **ask**" + private static final String LOG_EMBED_DESCRIPTION = "%s tag **%s**"; + + private static final String CONTENT_FILE_NAME = "content.md"; + private static final String NEW_CONTENT_FILE_NAME = "new_content.md"; + private static final String PREVIOUS_CONTENT_FILE_NAME = "previous_content.md"; + + private static final String UNABLE_TO_GET_CONTENT_MESSAGE = "Was unable to retrieve content"; + private final TagSystem tagSystem; private final Predicate hasRequiredRole; + private final ModAuditLogWriter modAuditLogWriter; + /** * Creates a new instance, using the given tag system as base. * * @param tagSystem the system providing the actual tag data * @param config the config to use for this + * @param modAuditLogWriter to log tag changes for audition */ - public TagManageCommand(TagSystem tagSystem, @NotNull Config config) { + public TagManageCommand(@NotNull TagSystem tagSystem, @NotNull Config config, + @NotNull ModAuditLogWriter modAuditLogWriter) { super("tag-manage", "Provides commands to manage all tags", SlashCommandVisibility.GUILD); this.tagSystem = tagSystem; hasRequiredRole = Pattern.compile(config.getTagManageRolePattern()).asMatchPredicate(); + this.modAuditLogWriter = modAuditLogWriter; + // TODO Think about adding a "Are you sure"-dialog to 'edit', 'edit-with-message' and // 'delete' getData().addSubcommands(new SubcommandData(Subcommand.RAW.name, @@ -155,31 +178,37 @@ private void rawTag(@NotNull SlashCommandEvent event) { } String content = tagSystem.getTag(id).orElseThrow(); - event.reply("").addFile(content.getBytes(StandardCharsets.UTF_8), "content.md").queue(); + event.reply("") + .addFile(content.getBytes(StandardCharsets.UTF_8), CONTENT_FILE_NAME) + .queue(); } private void createTag(@NotNull CommandInteraction event) { String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString(); - handleAction(TagStatus.NOT_EXISTS, id -> tagSystem.putTag(id, content), "created", event); + handleAction(TagStatus.NOT_EXISTS, id -> tagSystem.putTag(id, content), event, + Subcommand.CREATE, content); } private void createTagWithMessage(@NotNull CommandInteraction event) { - handleActionWithMessage(TagStatus.NOT_EXISTS, tagSystem::putTag, "created", event); + handleActionWithMessage(TagStatus.NOT_EXISTS, tagSystem::putTag, event, + Subcommand.CREATE_WITH_MESSAGE); } private void editTag(@NotNull CommandInteraction event) { String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString(); - handleAction(TagStatus.EXISTS, id -> tagSystem.putTag(id, content), "edited", event); + handleAction(TagStatus.EXISTS, id -> tagSystem.putTag(id, content), event, Subcommand.EDIT, + content); } private void editTagWithMessage(@NotNull CommandInteraction event) { - handleActionWithMessage(TagStatus.EXISTS, tagSystem::putTag, "edited", event); + handleActionWithMessage(TagStatus.EXISTS, tagSystem::putTag, event, + Subcommand.EDIT_WITH_MESSAGE); } private void deleteTag(@NotNull CommandInteraction event) { - handleAction(TagStatus.EXISTS, tagSystem::deleteTag, "deleted", event); + handleAction(TagStatus.EXISTS, tagSystem::deleteTag, event, Subcommand.DELETE, null); } /** @@ -190,20 +219,28 @@ private void deleteTag(@NotNull CommandInteraction event) { * * @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 + * @param subcommand the subcommand to be executed + * @param newContent the new content of the tag, or null if content is unchanged */ private void handleAction(@NotNull TagStatus requiredTagStatus, - @NotNull Consumer idAction, @NotNull String actionVerb, - @NotNull CommandInteraction event) { + @NotNull Consumer idAction, @NotNull CommandInteraction event, + @NotNull Subcommand subcommand, @Nullable String newContent) { + String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); if (isWrongTagStatusAndHandle(requiredTagStatus, id, event)) { return; } + String previousContent = + getTagContent(subcommand, id).orElse(UNABLE_TO_GET_CONTENT_MESSAGE); + idAction.accept(id); - sendSuccessMessage(event, id, actionVerb); + sendSuccessMessage(event, id, subcommand.getActionVerb()); + + Guild guild = Objects.requireNonNull(event.getGuild()); + logAction(subcommand, guild, event.getUser(), event.getTimeCreated(), id, newContent, + previousContent); } /** @@ -217,14 +254,14 @@ private void handleAction(@NotNull TagStatus requiredTagStatus, * * @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 + * @param subcommand the subcommand to be executed */ private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus, @NotNull BiConsumer idAndContentAction, - @NotNull String actionVerb, @NotNull CommandInteraction event) { + @NotNull CommandInteraction event, @NotNull Subcommand subcommand) { + String tagId = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString(); OptionalLong messageIdOpt = parseMessageIdAndHandle( Objects.requireNonNull(event.getOption(MESSAGE_ID_OPTION)).getAsString(), event); @@ -237,8 +274,16 @@ private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus, } event.getMessageChannel().retrieveMessageById(messageId).queue(message -> { + String previousContent = + getTagContent(subcommand, tagId).orElse(UNABLE_TO_GET_CONTENT_MESSAGE); + idAndContentAction.accept(tagId, message.getContentRaw()); - sendSuccessMessage(event, tagId, actionVerb); + sendSuccessMessage(event, tagId, subcommand.getActionVerb()); + + Guild guild = Objects.requireNonNull(event.getGuild()); + logAction(subcommand, guild, event.getUser(), event.getTimeCreated(), tagId, + message.getContentRaw(), previousContent); + }, failure -> { if (failure instanceof ErrorResponseException ex && ex.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) { @@ -258,6 +303,30 @@ private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus, }); } + /** + * Gets the content of a tag. + * + * @param subcommand the subcommand to be executed + * @param id the id of the tag to get its content + * @return the content of the tag, if present + */ + private @NotNull Optional getTagContent(@NotNull Subcommand subcommand, + @NotNull String id) { + if (Subcommand.SUBCOMMANDS_WITH_PREVIOUS_CONTENT.contains(subcommand)) { + try { + return tagSystem.getTag(id); + } catch (NoSuchElementException e) { + // NOTE Rare race condition, for example if another thread deleted the tag in the + // meantime + logger.warn(String.format( + "tried to retrieve content of tag '%s', but the content doesn't exist.", + id)); + } + } + + return Optional.empty(); + } + /** * Returns whether the status of the given tag is not equal to the required status. *

@@ -285,6 +354,51 @@ private boolean isWrongTagStatusAndHandle(@NotNull TagStatus requiredTagStatus, return false; } + private void logAction(@NotNull Subcommand subcommand, @NotNull Guild guild, + @NotNull User author, @NotNull TemporalAccessor triggeredAt, @NotNull String id, + @Nullable String newContent, @Nullable String previousContent) { + + List attachments = new ArrayList<>(); + + if (Subcommand.SUBCOMMANDS_WITH_NEW_CONTENT.contains(subcommand)) { + if (newContent == null) { + throw new IllegalArgumentException( + "newContent is null even though the subcommand should supply a value."); + } + + String fileName = (subcommand == Subcommand.CREATE + || subcommand == Subcommand.CREATE_WITH_MESSAGE) ? CONTENT_FILE_NAME + : NEW_CONTENT_FILE_NAME; + + attachments.add(new ModAuditLogWriter.Attachment(fileName, newContent)); + + } + + if (Subcommand.SUBCOMMANDS_WITH_PREVIOUS_CONTENT.contains(subcommand)) { + if (previousContent == null) { + throw new IllegalArgumentException( + "previousContent is null even though the subcommand should supply a value."); + } + + attachments + .add(new ModAuditLogWriter.Attachment(PREVIOUS_CONTENT_FILE_NAME, previousContent)); + } + + String title = switch (subcommand) { + case CREATE -> "Tag-Manage Create"; + case CREATE_WITH_MESSAGE -> "Tag-Manage Create with message"; + case EDIT -> "Tag-Manage Edit"; + case EDIT_WITH_MESSAGE -> "Tag-Manage Edit with message"; + case DELETE -> "Tag-Manage Delete"; + default -> throw new IllegalArgumentException( + "The subcommand '%s' is not intended to be logged to the mod audit channel."); + }; + + modAuditLogWriter.write(title, + LOG_EMBED_DESCRIPTION.formatted(subcommand.getActionVerb(), id), author, + triggeredAt, guild, attachments.toArray(ModAuditLogWriter.Attachment[]::new)); + } + private boolean hasTagManageRole(@NotNull Member member) { return member.getRoles().stream().map(Role::getName).anyMatch(hasRequiredRole); } @@ -296,17 +410,25 @@ private enum TagStatus { enum Subcommand { - RAW("raw"), - CREATE("create"), - CREATE_WITH_MESSAGE("create-with-message"), - EDIT("edit"), - EDIT_WITH_MESSAGE("edit-with-message"), - DELETE("delete"); + RAW("raw", ""), + CREATE("create", "created"), + CREATE_WITH_MESSAGE("create-with-message", "created"), + EDIT("edit", "edited"), + EDIT_WITH_MESSAGE("edit-with-message", "edited"), + DELETE("delete", "deleted"); + + private static final Set SUBCOMMANDS_WITH_NEW_CONTENT = + EnumSet.of(CREATE, CREATE_WITH_MESSAGE, EDIT, EDIT_WITH_MESSAGE); + private static final Set SUBCOMMANDS_WITH_PREVIOUS_CONTENT = + EnumSet.of(EDIT, EDIT_WITH_MESSAGE, DELETE); + private final String name; + private final String actionVerb; - Subcommand(@NotNull String name) { + Subcommand(@NotNull String name, @NotNull String actionVerb) { this.name = name; + this.actionVerb = actionVerb; } @NotNull @@ -323,5 +445,10 @@ static Subcommand fromName(@NotNull String name) { throw new IllegalArgumentException( "Subcommand with name '%s' is unknown".formatted(name)); } + + @NotNull + String getActionVerb() { + return actionVerb; + } } } diff --git a/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java b/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java new file mode 100644 index 0000000000..b7a1f9fa39 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java @@ -0,0 +1,118 @@ +package org.togetherjava.tjbot.moderation; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.requests.restaction.MessageAction; +import net.dv8tion.jda.api.utils.AttachmentOption; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.config.Config; + +import java.awt.*; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.time.temporal.TemporalAccessor; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Utility class that allows you to easily log an entry on the mod audit log channel. Thread-Safe. + *
+ *
+ * Use {@link ModAuditLogWriter#write(String, String, User, TemporalAccessor, Guild, Attachment...)} + * to log an entry. + */ +public final class ModAuditLogWriter { + private static final Color EMBED_COLOR = Color.decode("#3788AC"); + + private static final Logger logger = LoggerFactory.getLogger(ModAuditLogWriter.class); + + private final Config config; + + private final Predicate auditLogChannelNamePredicate; + + /** + * Creates a new instance. + * + * @param config the config to use for this + */ + public ModAuditLogWriter(@NotNull Config config) { + this.config = config; + auditLogChannelNamePredicate = + Pattern.compile(config.getModAuditLogChannelPattern()).asMatchPredicate(); + } + + /** + * Sends a log on the mod audit log channel. + * + * @param title the title of the log embed + * @param description the description of the log embed + * @param author the author of the log message + * @param timestamp the timestamp of the log message + * @param guild the guild to write this log to + * @param attachments attachments that will be added to the message. none or many. + */ + public void write(@NotNull String title, @NotNull String description, @NotNull User author, + @NotNull TemporalAccessor timestamp, @NotNull Guild guild, + @NotNull Attachment... attachments) { + Optional auditLogChannel = getAndHandleModAuditLogChannel(guild); + if (auditLogChannel.isEmpty()) { + return; + } + + MessageAction message = auditLogChannel.orElseThrow() + .sendMessageEmbeds(new EmbedBuilder().setTitle(title) + .setDescription(description) + .setAuthor(author.getAsTag(), null, author.getAvatarUrl()) + .setTimestamp(timestamp) + .setColor(EMBED_COLOR) + .build()); + + for (Attachment attachment : attachments) { + message = message.addFile(attachment.getContentRaw(), attachment.name()); + } + message.queue(); + } + + /** + * Gets the channel used for moderation audit logs, if present. If the channel doesn't exist, + * this method will return an empty optional, and a warning message will be written. + * + * @param guild the guild to look for the channel in + */ + public Optional getAndHandleModAuditLogChannel(@NotNull Guild guild) { + Optional auditLogChannel = guild.getTextChannelCache() + .stream() + .filter(channel -> auditLogChannelNamePredicate.test(channel.getName())) + .findAny(); + + if (auditLogChannel.isEmpty()) { + logger.warn( + "Unable to log moderation events, did not find a mod audit log channel matching the configured pattern '{}' for guild '{}'", + config.getModAuditLogChannelPattern(), guild.getName()); + } + return auditLogChannel; + } + + /** + * Represents attachment to messages, as for example used by + * {@link MessageAction#addFile(File, String, AttachmentOption...)}. + * + * @param name the name of the attachment, example: {@code "foo.md"} + * @param content the content of the attachment + */ + public record Attachment(@NotNull String name, @NotNull String content) { + /** + * Gets the content raw, interpreted as UTF-8. + * + * @return the raw content of the attachment + */ + public byte @NotNull [] getContentRaw() { + return content.getBytes(StandardCharsets.UTF_8); + } + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java b/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java index e3beb603dd..1cf837c809 100644 --- a/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java @@ -30,10 +30,10 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; -import java.util.function.Predicate; -import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.togetherjava.tjbot.moderation.ModAuditLogWriter; + /** * Routine that automatically checks moderator actions on a schedule and logs them to dedicated * channels. @@ -49,25 +49,22 @@ public final class ModAuditLogRoutine implements Routine { private static final int HOURS_OF_DAY = 24; private static final Color AMBIENT_COLOR = Color.decode("#4FC3F7"); - private final String modAuditLogChannelPattern; - private final Predicate isAuditLogChannel; private final Database database; private final Config config; + private final ModAuditLogWriter modAuditLogWriter; /** * Creates a new instance. * * @param database the database for memorizing audit log dates * @param config the config to use for this + * @param modAuditLogWriter to log tag changes for audition */ - public ModAuditLogRoutine(@NotNull Database database, @NotNull Config config) { - modAuditLogChannelPattern = config.getModAuditLogChannelPattern(); - Predicate isAuditLogChannelName = - Pattern.compile(modAuditLogChannelPattern).asMatchPredicate(); - isAuditLogChannel = channel -> isAuditLogChannelName.test(channel.getName()); - + public ModAuditLogRoutine(@NotNull Database database, @NotNull Config config, + @NotNull ModAuditLogWriter modAuditLogWriter) { this.config = config; this.database = database; + this.modAuditLogWriter = modAuditLogWriter; } private static @NotNull RestAction handleAction(@NotNull Action action, @@ -221,11 +218,9 @@ private void checkAuditLogsRoutine(@NotNull JDA jda) { return; } - Optional auditLogChannel = getModAuditLogChannel(guild); + Optional auditLogChannel = + modAuditLogWriter.getAndHandleModAuditLogChannel(guild); if (auditLogChannel.isEmpty()) { - logger.warn( - "Unable to log moderation events, did not find a mod audit log channel matching the configured pattern '{}' for guild '{}'", - modAuditLogChannelPattern, guild.getName()); return; } @@ -317,15 +312,6 @@ private boolean containsMutedRole(@NotNull AuditLogEntry entry, @NotNull AuditLo .anyMatch(ModerationUtils.getIsMutedRolePredicate(config)); } - private Optional getModAuditLogChannel(@NotNull Guild guild) { - // Check cache first, then get full list - return guild.getTextChannelCache() - .stream() - .filter(isAuditLogChannel) - .findAny() - .or(() -> guild.getTextChannels().stream().filter(isAuditLogChannel).findAny()); - } - private enum Action { BAN("Ban", "banned"), UNBAN("Unban", "unbanned"), diff --git a/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagManageCommandTest.java b/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagManageCommandTest.java index 591b85db89..70ba13ed88 100644 --- a/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagManageCommandTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagManageCommandTest.java @@ -18,6 +18,7 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.Tags; import org.togetherjava.tjbot.jda.JdaTester; +import org.togetherjava.tjbot.moderation.ModAuditLogWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -33,6 +34,7 @@ final class TagManageCommandTest { private JdaTester jdaTester; private SlashCommand command; private Member moderator; + private ModAuditLogWriter modAuditLogWriter; private static @NotNull MessageEmbed getResponse(@NotNull SlashCommandEvent event) { ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(MessageEmbed.class); @@ -45,11 +47,12 @@ void setUp() { Config config = mock(Config.class); String moderatorRoleName = "Moderator"; when(config.getTagManageRolePattern()).thenReturn(moderatorRoleName); + modAuditLogWriter = mock(ModAuditLogWriter.class); Database database = Database.createMemoryDatabase(Tags.TAGS); system = spy(new TagSystem(database)); jdaTester = new JdaTester(); - command = new TagManageCommand(system, config); + command = new TagManageCommand(system, config, modAuditLogWriter); moderator = jdaTester.createMemberSpy(1); Role moderatorRole = mock(Role.class); @@ -157,6 +160,7 @@ void commandCanNotBeUsedWithoutRoles() { // THEN the command can not be used since the user lacks roles verify(event).reply("Tags can only be managed by users with a corresponding role."); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -168,6 +172,7 @@ void rawTagCanNotFindUnknownTag() { // THEN the command can not find the tag and responds accordingly verify(event).reply(startsWith("Could not find any tag")); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -182,6 +187,7 @@ void rawTagShowsContentIfFound() { // THEN the command responds with its content as an attachment verify(jdaTester.getReplyActionMock()) .addFile(aryEq("bar".getBytes(StandardCharsets.UTF_8)), anyString()); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -197,6 +203,7 @@ void createTagThatAlreadyExistsFails() { verify(event).reply("The tag with id 'foo' already exists."); assertTrue(system.hasTag("foo")); assertEquals("old", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -210,6 +217,9 @@ void createNewTagWorks() { assertEquals("Success", getResponse(event).getTitle()); assertTrue(system.hasTag("foo")); assertEquals("bar", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter).write("Tag-Manage Create", "created tag **foo**", event.getUser(), + event.getTimeCreated(), event.getGuild(), + new ModAuditLogWriter.Attachment("content.md", "bar")); } @Test @@ -222,6 +232,7 @@ void editUnknownTagFails() { // THEN the command fails and responds accordingly, the tag was not created verify(event).reply(startsWith("Could not find any tag with id")); assertFalse(system.hasTag("foo")); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -236,6 +247,10 @@ void editExistingTagWorks() { // THEN the command succeeds and the content of the tag was changed assertEquals("Success", getResponse(event).getTitle()); assertEquals("new", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter).write("Tag-Manage Edit", "edited tag **foo**", event.getUser(), + event.getTimeCreated(), event.getGuild(), + new ModAuditLogWriter.Attachment("new_content.md", "new"), + new ModAuditLogWriter.Attachment("previous_content.md", "old")); } @Test @@ -247,6 +262,7 @@ void deleteUnknownTagFails() { // THEN the command fails and responds accordingly verify(event).reply(startsWith("Could not find any tag with id")); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -261,6 +277,9 @@ void deleteExistingTagWorks() { // THEN the command succeeds and the tag was deleted assertEquals("Success", getResponse(event).getTitle()); assertFalse(system.hasTag("foo")); + verify(modAuditLogWriter).write("Tag-Manage Delete", "deleted tag **foo**", event.getUser(), + event.getTimeCreated(), event.getGuild(), + new ModAuditLogWriter.Attachment("previous_content.md", "bar")); } @Test @@ -273,6 +292,7 @@ void createWithMessageFailsForInvalidMessageId() { // THEN the command fails and responds accordingly, the tag was not created verify(event).reply("The given message id 'bar' is invalid, expected a number."); assertFalse(system.hasTag("foo")); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -289,6 +309,7 @@ void createWithMessageTagThatAlreadyExistsFails() { verify(event).reply("The tag with id 'foo' already exists."); assertTrue(system.hasTag("foo")); assertEquals("old", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -304,6 +325,9 @@ void createWithMessageNewTagWorks() { assertEquals("Success", getResponse(event).getTitle()); assertTrue(system.hasTag("foo")); assertEquals("bar", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter).write("Tag-Manage Create with message", "created tag **foo**", + event.getUser(), event.getTimeCreated(), event.getGuild(), + new ModAuditLogWriter.Attachment("content.md", "bar")); } @Test @@ -319,6 +343,7 @@ void createWithMessageUnknownMessageFails() { // THEN the command fails and responds accordingly, the tag was not created verify(event).reply("The message with id '1' does not exist."); assertFalse(system.hasTag("foo")); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -333,6 +358,7 @@ void createWithMessageGenericErrorFails() { // THEN the command fails and responds accordingly, the tag was not created verify(event).reply(startsWith("Something unexpected went wrong")); assertFalse(system.hasTag("foo")); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -347,6 +373,7 @@ void editWithMessageFailsForInvalidMessageId() { // THEN the command fails and responds accordingly, the tags content was not changed verify(event).reply("The given message id 'bar' is invalid, expected a number."); assertEquals("old", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -361,6 +388,7 @@ void editWithMessageUnknownTagFails() { // THEN the command fails and responds accordingly, the tag was not created verify(event).reply(startsWith("Could not find any tag with id")); assertFalse(system.hasTag("foo")); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -376,6 +404,10 @@ void editWithMessageExistingTagWorks() { // THEN the command succeeds and the content of the tag was changed assertEquals("Success", getResponse(event).getTitle()); assertEquals("new", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter).write("Tag-Manage Edit with message", "edited tag **foo**", + event.getUser(), event.getTimeCreated(), event.getGuild(), + new ModAuditLogWriter.Attachment("new_content.md", "new"), + new ModAuditLogWriter.Attachment("previous_content.md", "old")); } @Test @@ -393,6 +425,7 @@ void editWithMessageUnknownMessageFails() { verify(event).reply("The message with id '1' does not exist."); assertTrue(system.hasTag("foo")); assertEquals("old", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } @Test @@ -409,5 +442,6 @@ void editWithMessageGenericErrorFails() { verify(event).reply(startsWith("Something unexpected went wrong")); assertTrue(system.hasTag("foo")); assertEquals("old", system.getTag("foo").orElseThrow()); + verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any()); } }