11package org .togetherjava .tjbot .commands .tags ;
22
33import net .dv8tion .jda .api .EmbedBuilder ;
4+ import net .dv8tion .jda .api .entities .Guild ;
45import net .dv8tion .jda .api .entities .Member ;
56import net .dv8tion .jda .api .entities .Role ;
7+ import net .dv8tion .jda .api .entities .User ;
68import net .dv8tion .jda .api .events .interaction .SlashCommandEvent ;
79import net .dv8tion .jda .api .exceptions .ErrorResponseException ;
810import net .dv8tion .jda .api .interactions .Interaction ;
1113import net .dv8tion .jda .api .interactions .commands .build .SubcommandData ;
1214import net .dv8tion .jda .api .requests .ErrorResponse ;
1315import org .jetbrains .annotations .NotNull ;
16+ import org .jetbrains .annotations .Nullable ;
1417import org .slf4j .Logger ;
1518import org .slf4j .LoggerFactory ;
1619import org .togetherjava .tjbot .commands .SlashCommandAdapter ;
1720import org .togetherjava .tjbot .commands .SlashCommandVisibility ;
1821import 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 ;
2027import java .nio .charset .StandardCharsets ;
2128import java .time .Instant ;
2229import 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