Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logging the /tag-manage command #296

Merged
merged 77 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from 76 commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
eb273b9
`minor fix`
Dec 2, 2021
b0ce95d
fixed auto-checked problems
Dec 3, 2021
e06af67
`minor fix`
Dec 3, 2021
5545a72
added `content.md` files to the logs and optimized logging methods to…
Dec 3, 2021
3c8c59c
removed duplicate `getTextChannels()`
Dec 3, 2021
202e117
made `log()` `synchronized` to prevent interruptions
Dec 3, 2021
a4447cf
made `enum Filename`
Dec 3, 2021
ccbaecb
files are now in the same message as the embed
Dec 4, 2021
b9f98fa
changed `oldContent` to `previousContent`
Dec 4, 2021
a78c039
changed `class VirtualFile` to `record Attachment`
Dec 4, 2021
40ce619
made `class ModAuditLogWriter` an `enum`
Dec 4, 2021
4bfb896
fixed minor code-style problems
Dec 4, 2021
d7e3ccf
improved the structure of `ModAuditLogWriter#log()`
Dec 5, 2021
60e328b
merged the 3 logging methods in `TagManageCommand`
Dec 5, 2021
123aa43
now using `Optional#orElseThrow()` instead of `Optional#get()`
Dec 5, 2021
d5438e6
changed error message `"Shouldn't log subcommand"`
Dec 5, 2021
c4e752b
made arrays `@NotNull`
Dec 5, 2021
e94a720
replaced `Filename` enum with constants
Dec 7, 2021
3c9658e
now checking for `null` instead of abusing Optionals
Dec 7, 2021
81b43b3
`ModAuditLogWriter#log` now takes the attachments as a List instead o…
Dec 7, 2021
e9e4a25
updated a minor javadoc
Dec 7, 2021
35411af
renamed `ModAuditLogWriter#log()` to `writeModAuditLog()`
Dec 7, 2021
b462051
changed the embed color from `#4FC3F7` to `#3788AC`
Dec 7, 2021
d0f3340
removed duplicate `getModAuditLogChannel()`
Dec 7, 2021
3e3a7b6
now ignores `NoSuchElementException`
Dec 7, 2021
70c680c
`actionVerb` is now a field in `Subcommand` enum
Dec 7, 2021
64aaab4
removed unused field and var
Dec 7, 2021
91a968d
moved duplicate debug message
Dec 7, 2021
6f6a342
removed static import
interacsion Dec 24, 2021
ae123c5
Improved JavaDoc.
interacsion Jan 20, 2022
b23e7f5
Added missing imports
interacsion Jan 21, 2022
54c9daf
removed `static final` from `record`
interacsion Jan 21, 2022
071fc9b
Slightly updated previous content retrieval code
interacsion Jan 21, 2022
b494b75
now using `EnumSet` instead of `List`
interacsion Jan 21, 2022
5bc61ff
made `logAction()` fail-fast
interacsion Jan 21, 2022
358057c
removed unnecessary constructor
interacsion Jan 21, 2022
4377d89
made `Subcommand` enum methods `@NotNull`
interacsion Jan 21, 2022
6eb5898
moved predicates directly into the variable declaration
interacsion Jan 21, 2022
e8a7f0b
updated `Attachment` `record`
interacsion Jan 21, 2022
7c6823d
moved the code that gets previous content into a helper method, getTa…
interacsion Jan 21, 2022
a221436
now using `actionVerb` as the verb in the message
interacsion Jan 21, 2022
24def34
renamed `writeModAuditLog()` to `write`
interacsion Jan 21, 2022
7c49c73
removed `write()` overloads and now getting attachments as a vararg
interacsion Jan 21, 2022
a9a4339
renamed `getModAuditLogChannel()` to `getAndHandleModAuditLogChannel()`
interacsion Jan 21, 2022
81482cd
fixed code-style.
interacsion Jan 21, 2022
1a8b799
fixed a merge issue
interacsion Jan 26, 2022
429d9b7
using constant `CONTENT_FILE_NAME` instead of `"content.md"`
interacsion Jan 27, 2022
d011b34
Now using a constant for the log embed description
interacsion Jan 27, 2022
3b9b8b9
code-style quick fix
interacsion Jan 27, 2022
749db7b
moved comment to be above the constant, instead of in front of it.
interacsion Jan 27, 2022
7d25c29
Put audit log channel name pattern in a field
interacsion Jan 27, 2022
97c4f32
renamed field.
interacsion Jan 27, 2022
1cf7627
spotless applied.
interacsion Jan 28, 2022
ccac112
Removed useless imports.
interacsion Feb 9, 2022
f00590a
Updated `getTagContent()`
interacsion Mar 2, 2022
e848ac0
Some code changes
interacsion Mar 11, 2022
4d1f09a
Updated `ModAuditLogWriter`'s javadoc
interacsion Mar 11, 2022
4d55f1f
changed variable name
interacsion Mar 11, 2022
f8f6608
updated `getTagContent()`
interacsion Mar 11, 2022
86b93e5
Moved enum set into a variable.
interacsion Mar 11, 2022
2521bcb
code reformat
interacsion Mar 11, 2022
1a67ae9
code reformat
interacsion Mar 11, 2022
f036077
moved field and var into `Subcommand`
interacsion Mar 11, 2022
7b101c6
made fields `private`
interacsion Mar 11, 2022
4b20e3c
removed unnecessary `this.`
interacsion Mar 11, 2022
da0b177
Rebased and made `ModAuditLogWriter` singleton
interacsion Mar 11, 2022
ddc1b88
Using a `BiConsumer` to minimize code duplication.
interacsion Mar 11, 2022
2661399
`getTagContent()` is now returning `null`
interacsion Mar 11, 2022
f6ad120
Made test compile again
interacsion Mar 11, 2022
1f493f3
now verifying wanted results in tests
interacsion Mar 11, 2022
a3692ba
quick patch
interacsion Mar 12, 2022
7a812b7
Updated `Unable to retrieve content` handling
interacsion Mar 12, 2022
4a19004
made `auditLogChannelNamePattern` compile in constructor
interacsion Mar 12, 2022
279d3aa
changed `logAction()` logic
interacsion Mar 12, 2022
7639f71
Updated `getTagContent()` javadoc
interacsion Mar 12, 2022
2111dc8
replaced `ArrayList` with `List`
interacsion Mar 12, 2022
cf98c4a
put predicate in the field instead of the pattern
interacsion Mar 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,14 +51,15 @@ 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
// available.
Collection<Feature> 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));
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> 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,
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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. <i>edited</i> or
* <i>created</i>, 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<? super String> idAction, @NotNull String actionVerb,
@NotNull CommandInteraction event) {
@NotNull Consumer<? super String> 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);
}

/**
Expand All @@ -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. <i>edited</i> or
* <i>created</i>, 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<? super String, ? super String> 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);
Expand All @@ -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) {
Expand All @@ -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<String> 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 <b>not equal</b> to the required status.
* <p>
Expand Down Expand Up @@ -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<ModAuditLogWriter.Attachment> 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);
}
Expand All @@ -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<Subcommand> SUBCOMMANDS_WITH_NEW_CONTENT =
EnumSet.of(CREATE, CREATE_WITH_MESSAGE, EDIT, EDIT_WITH_MESSAGE);
private static final Set<Subcommand> 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
Expand All @@ -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;
}
}
}
Loading