diff --git a/Commands/UserNoteCmds.cs b/Commands/UserNoteCmds.cs index 1a688d07..ecab9bce 100644 --- a/Commands/UserNoteCmds.cs +++ b/Commands/UserNoteCmds.cs @@ -13,6 +13,7 @@ public class UserNoteCmds public async Task AddUserNoteAsync(SlashCommandContext ctx, [Parameter("user"), Description("The user to add a note for.")] DiscordUser user, [Parameter("note"), Description("The note to add.")] string noteText, + [Parameter("expires"), Description("When the note should automatically expire. Default: never")] string expires = default, [Parameter("show_on_modmail"), Description("Whether to show the note when the user opens a modmail thread. Default: true")] bool showOnModmail = true, [Parameter("show_on_warn"), Description("Whether to show the note when the user is warned. Default: true")] bool showOnWarn = true, [Parameter("show_all_mods"), Description("Whether to show this note to all mods, versus just yourself. Default: true")] bool showAllMods = true, @@ -21,6 +22,28 @@ public async Task AddUserNoteAsync(SlashCommandContext ctx, { await ctx.DeferResponseAsync(); + DateTime? expireTime; + if (expires is null || expires.Equals("never", StringComparison.OrdinalIgnoreCase)) + expireTime = default; + else + { + try + { + expireTime = HumanDateParser.HumanDateParser.Parse(expires).ToUniversalTime(); + } + catch + { + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I couldn't parse the provided expiration time!")); + return; + } + + if (expireTime <= DateTime.UtcNow) + { + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} Notes can't be set to expire in the past!")); + return; + } + } + // Assemble new note long noteId = Program.redis.StringIncrement("totalWarnings"); UserNote note = new() @@ -35,11 +58,15 @@ public async Task AddUserNoteAsync(SlashCommandContext ctx, ShowOnJoinAndLeave = showOnJoinAndLeave, NoteId = noteId, Timestamp = DateTime.UtcNow, - Type = WarningType.Note + Type = WarningType.Note, + ExpireTime = expireTime, }; await Program.redis.HashSetAsync(user.Id.ToString(), note.NoteId, JsonConvert.SerializeObject(note)); + if (expireTime != default) + await Program.redis.HashSetAsync("expiringNotes", note.NoteId, JsonConvert.SerializeObject(note)); + // Log to mod-logs var embed = await GenerateUserNoteDetailEmbedAsync(note, user); await LogChannelHelper.LogMessageAsync("mod", $"{Program.cfgjson.Emoji.Information} New note for {user.Mention}!", embed); @@ -89,6 +116,7 @@ public async Task RemoveUserNoteAsync(SlashCommandContext ctx, public async Task EditUserNoteAsync(SlashCommandContext ctx, [Parameter("user"), Description("The user to edit a note for.")] DiscordUser user, [SlashAutoCompleteProvider(typeof(NotesAutocompleteProvider))][Parameter("note"), Description("The note to edit.")] string targetNote, + [Parameter("expires"), Description("When the note should automatically expire. Type \"never\" to disable expiry.")] string expires = default, [Parameter("new_text"), Description("The new note text. Leave empty to not change.")] string newNoteText = default, [Parameter("show_on_modmail"), Description("Whether to show the note when the user opens a modmail thread.")] bool? showOnModmail = null, [Parameter("show_on_warn"), Description("Whether to show the note when the user is warned.")] bool? showOnWarn = null, @@ -113,7 +141,7 @@ public async Task EditUserNoteAsync(SlashCommandContext ctx, newNoteText = note.NoteText; // If no changes are made, refuse the request - if (note.NoteText == newNoteText && showOnModmail is null && showOnWarn is null && showAllMods is null && showOnce is null && showOnJoinAndLeave is null) + if (note.NoteText == newNoteText && expires is null && showOnModmail is null && showOnWarn is null && showAllMods is null && showOnce is null && showOnJoinAndLeave is null) { await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You didn't change anything about the note!").AsEphemeral()); return; @@ -138,9 +166,38 @@ public async Task EditUserNoteAsync(SlashCommandContext ctx, if (showOnJoinAndLeave is null) showOnJoinAndLeave = note.ShowOnJoinAndLeave; + DateTime? expireTime; + if (expires == default) + { + expireTime = note.ExpireTime; + } + else if (expires.Equals("never", StringComparison.OrdinalIgnoreCase)) + { + expireTime = null; + } + else + { + try + { + expireTime = HumanDateParser.HumanDateParser.Parse(expires).ToUniversalTime(); + } + catch + { + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I couldn't parse the provided expiration time!")); + return; + } + + if (expireTime <= DateTime.UtcNow) + { + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} Notes can't be set to expire in the past!")); + return; + } + } + // Assemble new note note.ModUserId = ctx.User.Id; note.NoteText = newNoteText; + note.ExpireTime = expireTime; note.ShowOnModmail = (bool)showOnModmail; note.ShowOnWarn = (bool)showOnWarn; note.ShowAllMods = (bool)showAllMods; @@ -150,6 +207,18 @@ public async Task EditUserNoteAsync(SlashCommandContext ctx, await Program.redis.HashSetAsync(user.Id.ToString(), note.NoteId, JsonConvert.SerializeObject(note)); + if (note.ExpireTime is null) + { + // This is unnecessary if the note wasn't previously set to expire, but it doesn't seem problematic to + // call this even if the hash already does not exist. Saves a call to HashExistsAsync + await Program.redis.HashDeleteAsync("expiringNotes", note.NoteId); + } + else + { + // This might also be unnecessary (see above) but saves a HashGetAsync in case the expire time was changed + await Program.redis.HashSetAsync("expiringNotes", note.NoteId, JsonConvert.SerializeObject(note)); + } + // Log to mod-logs var embed = await GenerateUserNoteDetailEmbedAsync(note, user); await LogChannelHelper.LogMessageAsync("mod", $"{Program.cfgjson.Emoji.Information} Note edited: `{note.NoteId}` (belonging to {user.Mention})", embed); diff --git a/Helpers/UserNoteHelpers.cs b/Helpers/UserNoteHelpers.cs index 54c59989..6fc583dc 100644 --- a/Helpers/UserNoteHelpers.cs +++ b/Helpers/UserNoteHelpers.cs @@ -129,8 +129,9 @@ await LykosAvatarMethods.UserOrMemberAvatarURL(user, Program.homeGuild, "png") .AddField("Show all Mods", note.ShowAllMods ? "Yes" : "No", true) .AddField("Show Once", note.ShowOnce ? "Yes" : "No", true) .AddField("Show on Join & Leave", note.ShowOnJoinAndLeave ? "Yes" : "No", true) - .AddField("Responsible moderator", $"<@{note.ModUserId}>", true) - .AddField("Time", $"", true); + .AddField("Time", $"", true) + .AddField("Expires", note.ExpireTime == default ? "Never" : $"", true) + .AddField("Responsible moderator", $"<@{note.ModUserId}>", false); return embed; } diff --git a/Program.cs b/Program.cs index 215e70fc..3cf3969e 100644 --- a/Program.cs +++ b/Program.cs @@ -285,6 +285,7 @@ static async Task Main(string[] _) Tasks.PunishmentTasks.CheckMutesAsync(), Tasks.PunishmentTasks.CheckBansAsync(), Tasks.PunishmentTasks.CleanUpPunishmentMessagesAsync(), + Tasks.PunishmentTasks.CleanUpExpiredNotesAsync(), Tasks.ReminderTasks.CheckRemindersAsync(), Tasks.RaidmodeTasks.CheckRaidmodeAsync(cfgjson.ServerID), Tasks.LockdownTasks.CheckUnlocksAsync(), diff --git a/Tasks/PunishmentTasks.cs b/Tasks/PunishmentTasks.cs index af9dc134..f188d049 100644 --- a/Tasks/PunishmentTasks.cs +++ b/Tasks/PunishmentTasks.cs @@ -151,6 +151,32 @@ public static async Task CleanUpPunishmentMessagesAsync() Program.discord.Logger.LogDebug(Program.CliptokEventID, "Checked for auto-warn and compromised account ban messages at {time} with result: {result}", DateTime.UtcNow, success); return success; } + + public static async Task CleanUpExpiredNotesAsync() + { + var expiringNotes = await Program.redis.HashGetAllAsync("expiringNotes"); + + bool success = false; + + foreach (var note in expiringNotes.Select(x => JsonConvert.DeserializeObject(x.Value))) + { + if (note.ExpireTime is not null && note.ExpireTime < DateTime.UtcNow) + { + await Program.redis.HashDeleteAsync(note.TargetUserId.ToString(), note.NoteId); + await Program.redis.HashDeleteAsync("expiringNotes", note.NoteId); + + // Log to mod-logs + var user = await Program.discord.GetUserAsync(note.TargetUserId); + var embed = new DiscordEmbedBuilder(await UserNoteHelpers.GenerateUserNoteDetailEmbedAsync(note, user)).WithColor(0xf03916); + await LogChannelHelper.LogMessageAsync("mod", $"{Program.cfgjson.Emoji.Deleted} Note expired: `{note.NoteId}` (belonging to {user.Mention})", embed); + + success = true; + } + } + + Program.discord.Logger.LogDebug(Program.CliptokEventID, "Checked notes at {time} with result: {success}", DateTime.UtcNow, success); + return success; + } } } diff --git a/Types/RedisData.cs b/Types/RedisData.cs index 706dc5f3..fed62bad 100644 --- a/Types/RedisData.cs +++ b/Types/RedisData.cs @@ -109,6 +109,9 @@ public class UserNote [JsonProperty("type")] public WarningType Type { get; set; } + + [JsonProperty("expireTime")] + public DateTime? ExpireTime { get; set; } } public class PendingUserOverride