Skip to content

Commit dfa3696

Browse files
interacsionTais993
andauthored
Added ModAuditLogWriter and logging /tag changes
* Added ModAuditLogWriter as utility to write to the mod-audit-log channel * Made the TagSystem use the writer to log any changes on the tags, for example if a tag got edited * Adjusted unit tests of TagSystem to also test new writer logic Co-authored-by: Tais993 <[email protected]>
1 parent dff6068 commit dfa3696

5 files changed

Lines changed: 316 additions & 49 deletions

File tree

application/src/main/java/org/togetherjava/tjbot/commands/Features.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.togetherjava.tjbot.commands.tophelper.TopHelpersPurgeMessagesRoutine;
2020
import org.togetherjava.tjbot.config.Config;
2121
import org.togetherjava.tjbot.db.Database;
22+
import org.togetherjava.tjbot.moderation.ModAuditLogWriter;
2223
import org.togetherjava.tjbot.routines.ModAuditLogRoutine;
2324

2425
import java.util.ArrayList;
@@ -50,14 +51,15 @@ public enum Features {
5051
@NotNull Database database, @NotNull Config config) {
5152
TagSystem tagSystem = new TagSystem(database);
5253
ModerationActionsStore actionsStore = new ModerationActionsStore(database);
54+
ModAuditLogWriter modAuditLogWriter = new ModAuditLogWriter(config);
5355

5456
// NOTE The system can add special system relevant commands also by itself,
5557
// hence this list may not necessarily represent the full list of all commands actually
5658
// available.
5759
Collection<Feature> features = new ArrayList<>();
5860

5961
// Routines
60-
features.add(new ModAuditLogRoutine(database, config));
62+
features.add(new ModAuditLogRoutine(database, config, modAuditLogWriter));
6163
features.add(new TemporaryModerationRoutine(jda, actionsStore, config));
6264
features.add(new TopHelpersPurgeMessagesRoutine(database));
6365
features.add(new RemindRoutine(database));
@@ -73,7 +75,7 @@ public enum Features {
7375
features.add(new PingCommand());
7476
features.add(new TeXCommand());
7577
features.add(new TagCommand(tagSystem));
76-
features.add(new TagManageCommand(tagSystem, config));
78+
features.add(new TagManageCommand(tagSystem, config, modAuditLogWriter));
7779
features.add(new TagsCommand(tagSystem));
7880
features.add(new VcActivityCommand());
7981
features.add(new WarnCommand(actionsStore, config));

application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java

Lines changed: 150 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.togetherjava.tjbot.commands.tags;
22

33
import net.dv8tion.jda.api.EmbedBuilder;
4+
import net.dv8tion.jda.api.entities.Guild;
45
import net.dv8tion.jda.api.entities.Member;
56
import net.dv8tion.jda.api.entities.Role;
7+
import net.dv8tion.jda.api.entities.User;
68
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
79
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
810
import net.dv8tion.jda.api.interactions.Interaction;
@@ -11,12 +13,17 @@
1113
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
1214
import net.dv8tion.jda.api.requests.ErrorResponse;
1315
import org.jetbrains.annotations.NotNull;
16+
import org.jetbrains.annotations.Nullable;
1417
import org.slf4j.Logger;
1518
import org.slf4j.LoggerFactory;
1619
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
1720
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
1821
import org.togetherjava.tjbot.config.Config;
22+
import org.togetherjava.tjbot.moderation.ModAuditLogWriter;
1923

24+
import java.time.temporal.TemporalAccessor;
25+
import java.util.*;
26+
import java.util.NoSuchElementException;
2027
import java.nio.charset.StandardCharsets;
2128
import java.time.Instant;
2229
import java.util.Objects;
@@ -49,21 +56,37 @@ public final class TagManageCommand extends SlashCommandAdapter {
4956
private static final String CONTENT_DESCRIPTION = "the content of the tag";
5057
static final String MESSAGE_ID_OPTION = "message-id";
5158
private static final String MESSAGE_ID_DESCRIPTION = "the id of the message to refer to";
59+
60+
// "Edited tag **ask**"
61+
private static final String LOG_EMBED_DESCRIPTION = "%s tag **%s**";
62+
63+
private static final String CONTENT_FILE_NAME = "content.md";
64+
private static final String NEW_CONTENT_FILE_NAME = "new_content.md";
65+
private static final String PREVIOUS_CONTENT_FILE_NAME = "previous_content.md";
66+
67+
private static final String UNABLE_TO_GET_CONTENT_MESSAGE = "Was unable to retrieve content";
68+
5269
private final TagSystem tagSystem;
5370
private final Predicate<String> hasRequiredRole;
5471

72+
private final ModAuditLogWriter modAuditLogWriter;
73+
5574
/**
5675
* Creates a new instance, using the given tag system as base.
5776
*
5877
* @param tagSystem the system providing the actual tag data
5978
* @param config the config to use for this
79+
* @param modAuditLogWriter to log tag changes for audition
6080
*/
61-
public TagManageCommand(TagSystem tagSystem, @NotNull Config config) {
81+
public TagManageCommand(@NotNull TagSystem tagSystem, @NotNull Config config,
82+
@NotNull ModAuditLogWriter modAuditLogWriter) {
6283
super("tag-manage", "Provides commands to manage all tags", SlashCommandVisibility.GUILD);
6384

6485
this.tagSystem = tagSystem;
6586
hasRequiredRole = Pattern.compile(config.getTagManageRolePattern()).asMatchPredicate();
6687

88+
this.modAuditLogWriter = modAuditLogWriter;
89+
6790
// TODO Think about adding a "Are you sure"-dialog to 'edit', 'edit-with-message' and
6891
// 'delete'
6992
getData().addSubcommands(new SubcommandData(Subcommand.RAW.name,
@@ -155,31 +178,37 @@ private void rawTag(@NotNull SlashCommandEvent event) {
155178
}
156179

157180
String content = tagSystem.getTag(id).orElseThrow();
158-
event.reply("").addFile(content.getBytes(StandardCharsets.UTF_8), "content.md").queue();
181+
event.reply("")
182+
.addFile(content.getBytes(StandardCharsets.UTF_8), CONTENT_FILE_NAME)
183+
.queue();
159184
}
160185

161186
private void createTag(@NotNull CommandInteraction event) {
162187
String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString();
163188

164-
handleAction(TagStatus.NOT_EXISTS, id -> tagSystem.putTag(id, content), "created", event);
189+
handleAction(TagStatus.NOT_EXISTS, id -> tagSystem.putTag(id, content), event,
190+
Subcommand.CREATE, content);
165191
}
166192

167193
private void createTagWithMessage(@NotNull CommandInteraction event) {
168-
handleActionWithMessage(TagStatus.NOT_EXISTS, tagSystem::putTag, "created", event);
194+
handleActionWithMessage(TagStatus.NOT_EXISTS, tagSystem::putTag, event,
195+
Subcommand.CREATE_WITH_MESSAGE);
169196
}
170197

171198
private void editTag(@NotNull CommandInteraction event) {
172199
String content = Objects.requireNonNull(event.getOption(CONTENT_OPTION)).getAsString();
173200

174-
handleAction(TagStatus.EXISTS, id -> tagSystem.putTag(id, content), "edited", event);
201+
handleAction(TagStatus.EXISTS, id -> tagSystem.putTag(id, content), event, Subcommand.EDIT,
202+
content);
175203
}
176204

177205
private void editTagWithMessage(@NotNull CommandInteraction event) {
178-
handleActionWithMessage(TagStatus.EXISTS, tagSystem::putTag, "edited", event);
206+
handleActionWithMessage(TagStatus.EXISTS, tagSystem::putTag, event,
207+
Subcommand.EDIT_WITH_MESSAGE);
179208
}
180209

181210
private void deleteTag(@NotNull CommandInteraction event) {
182-
handleAction(TagStatus.EXISTS, tagSystem::deleteTag, "deleted", event);
211+
handleAction(TagStatus.EXISTS, tagSystem::deleteTag, event, Subcommand.DELETE, null);
183212
}
184213

185214
/**
@@ -190,20 +219,28 @@ private void deleteTag(@NotNull CommandInteraction event) {
190219
*
191220
* @param requiredTagStatus the required status of the tag
192221
* @param idAction the action to perform on the id
193-
* @param actionVerb the verb describing the executed action, i.e. <i>edited</i> or
194-
* <i>created</i>, will be displayed in the message send to the user
195222
* @param event the event to send messages with, it must have an {@code id} option set
223+
* @param subcommand the subcommand to be executed
224+
* @param newContent the new content of the tag, or null if content is unchanged
196225
*/
197226
private void handleAction(@NotNull TagStatus requiredTagStatus,
198-
@NotNull Consumer<? super String> idAction, @NotNull String actionVerb,
199-
@NotNull CommandInteraction event) {
227+
@NotNull Consumer<? super String> idAction, @NotNull CommandInteraction event,
228+
@NotNull Subcommand subcommand, @Nullable String newContent) {
229+
200230
String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
201231
if (isWrongTagStatusAndHandle(requiredTagStatus, id, event)) {
202232
return;
203233
}
204234

235+
String previousContent =
236+
getTagContent(subcommand, id).orElse(UNABLE_TO_GET_CONTENT_MESSAGE);
237+
205238
idAction.accept(id);
206-
sendSuccessMessage(event, id, actionVerb);
239+
sendSuccessMessage(event, id, subcommand.getActionVerb());
240+
241+
Guild guild = Objects.requireNonNull(event.getGuild());
242+
logAction(subcommand, guild, event.getUser(), event.getTimeCreated(), id, newContent,
243+
previousContent);
207244
}
208245

209246
/**
@@ -217,14 +254,14 @@ private void handleAction(@NotNull TagStatus requiredTagStatus,
217254
*
218255
* @param requiredTagStatus the required status of the tag
219256
* @param idAndContentAction the action to perform on the id and content
220-
* @param actionVerb the verb describing the executed action, i.e. <i>edited</i> or
221-
* <i>created</i>, will be displayed in the message send to the user
222257
* @param event the event to send messages with, it must have an {@code id} and
223258
* {@code message-id} option set
259+
* @param subcommand the subcommand to be executed
224260
*/
225261
private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus,
226262
@NotNull BiConsumer<? super String, ? super String> idAndContentAction,
227-
@NotNull String actionVerb, @NotNull CommandInteraction event) {
263+
@NotNull CommandInteraction event, @NotNull Subcommand subcommand) {
264+
228265
String tagId = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
229266
OptionalLong messageIdOpt = parseMessageIdAndHandle(
230267
Objects.requireNonNull(event.getOption(MESSAGE_ID_OPTION)).getAsString(), event);
@@ -237,8 +274,16 @@ private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus,
237274
}
238275

239276
event.getMessageChannel().retrieveMessageById(messageId).queue(message -> {
277+
String previousContent =
278+
getTagContent(subcommand, tagId).orElse(UNABLE_TO_GET_CONTENT_MESSAGE);
279+
240280
idAndContentAction.accept(tagId, message.getContentRaw());
241-
sendSuccessMessage(event, tagId, actionVerb);
281+
sendSuccessMessage(event, tagId, subcommand.getActionVerb());
282+
283+
Guild guild = Objects.requireNonNull(event.getGuild());
284+
logAction(subcommand, guild, event.getUser(), event.getTimeCreated(), tagId,
285+
message.getContentRaw(), previousContent);
286+
242287
}, failure -> {
243288
if (failure instanceof ErrorResponseException ex
244289
&& ex.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) {
@@ -258,6 +303,30 @@ private void handleActionWithMessage(@NotNull TagStatus requiredTagStatus,
258303
});
259304
}
260305

306+
/**
307+
* Gets the content of a tag.
308+
*
309+
* @param subcommand the subcommand to be executed
310+
* @param id the id of the tag to get its content
311+
* @return the content of the tag, if present
312+
*/
313+
private @NotNull Optional<String> getTagContent(@NotNull Subcommand subcommand,
314+
@NotNull String id) {
315+
if (Subcommand.SUBCOMMANDS_WITH_PREVIOUS_CONTENT.contains(subcommand)) {
316+
try {
317+
return tagSystem.getTag(id);
318+
} catch (NoSuchElementException e) {
319+
// NOTE Rare race condition, for example if another thread deleted the tag in the
320+
// meantime
321+
logger.warn(String.format(
322+
"tried to retrieve content of tag '%s', but the content doesn't exist.",
323+
id));
324+
}
325+
}
326+
327+
return Optional.empty();
328+
}
329+
261330
/**
262331
* Returns whether the status of the given tag is <b>not equal</b> to the required status.
263332
* <p>
@@ -285,6 +354,51 @@ private boolean isWrongTagStatusAndHandle(@NotNull TagStatus requiredTagStatus,
285354
return false;
286355
}
287356

357+
private void logAction(@NotNull Subcommand subcommand, @NotNull Guild guild,
358+
@NotNull User author, @NotNull TemporalAccessor triggeredAt, @NotNull String id,
359+
@Nullable String newContent, @Nullable String previousContent) {
360+
361+
List<ModAuditLogWriter.Attachment> attachments = new ArrayList<>();
362+
363+
if (Subcommand.SUBCOMMANDS_WITH_NEW_CONTENT.contains(subcommand)) {
364+
if (newContent == null) {
365+
throw new IllegalArgumentException(
366+
"newContent is null even though the subcommand should supply a value.");
367+
}
368+
369+
String fileName = (subcommand == Subcommand.CREATE
370+
|| subcommand == Subcommand.CREATE_WITH_MESSAGE) ? CONTENT_FILE_NAME
371+
: NEW_CONTENT_FILE_NAME;
372+
373+
attachments.add(new ModAuditLogWriter.Attachment(fileName, newContent));
374+
375+
}
376+
377+
if (Subcommand.SUBCOMMANDS_WITH_PREVIOUS_CONTENT.contains(subcommand)) {
378+
if (previousContent == null) {
379+
throw new IllegalArgumentException(
380+
"previousContent is null even though the subcommand should supply a value.");
381+
}
382+
383+
attachments
384+
.add(new ModAuditLogWriter.Attachment(PREVIOUS_CONTENT_FILE_NAME, previousContent));
385+
}
386+
387+
String title = switch (subcommand) {
388+
case CREATE -> "Tag-Manage Create";
389+
case CREATE_WITH_MESSAGE -> "Tag-Manage Create with message";
390+
case EDIT -> "Tag-Manage Edit";
391+
case EDIT_WITH_MESSAGE -> "Tag-Manage Edit with message";
392+
case DELETE -> "Tag-Manage Delete";
393+
default -> throw new IllegalArgumentException(
394+
"The subcommand '%s' is not intended to be logged to the mod audit channel.");
395+
};
396+
397+
modAuditLogWriter.write(title,
398+
LOG_EMBED_DESCRIPTION.formatted(subcommand.getActionVerb(), id), author,
399+
triggeredAt, guild, attachments.toArray(ModAuditLogWriter.Attachment[]::new));
400+
}
401+
288402
private boolean hasTagManageRole(@NotNull Member member) {
289403
return member.getRoles().stream().map(Role::getName).anyMatch(hasRequiredRole);
290404
}
@@ -296,17 +410,25 @@ private enum TagStatus {
296410

297411

298412
enum Subcommand {
299-
RAW("raw"),
300-
CREATE("create"),
301-
CREATE_WITH_MESSAGE("create-with-message"),
302-
EDIT("edit"),
303-
EDIT_WITH_MESSAGE("edit-with-message"),
304-
DELETE("delete");
413+
RAW("raw", ""),
414+
CREATE("create", "created"),
415+
CREATE_WITH_MESSAGE("create-with-message", "created"),
416+
EDIT("edit", "edited"),
417+
EDIT_WITH_MESSAGE("edit-with-message", "edited"),
418+
DELETE("delete", "deleted");
419+
420+
private static final Set<Subcommand> SUBCOMMANDS_WITH_NEW_CONTENT =
421+
EnumSet.of(CREATE, CREATE_WITH_MESSAGE, EDIT, EDIT_WITH_MESSAGE);
422+
private static final Set<Subcommand> SUBCOMMANDS_WITH_PREVIOUS_CONTENT =
423+
EnumSet.of(EDIT, EDIT_WITH_MESSAGE, DELETE);
424+
305425

306426
private final String name;
427+
private final String actionVerb;
307428

308-
Subcommand(@NotNull String name) {
429+
Subcommand(@NotNull String name, @NotNull String actionVerb) {
309430
this.name = name;
431+
this.actionVerb = actionVerb;
310432
}
311433

312434
@NotNull
@@ -323,5 +445,10 @@ static Subcommand fromName(@NotNull String name) {
323445
throw new IllegalArgumentException(
324446
"Subcommand with name '%s' is unknown".formatted(name));
325447
}
448+
449+
@NotNull
450+
String getActionVerb() {
451+
return actionVerb;
452+
}
326453
}
327454
}

0 commit comments

Comments
 (0)