diff --git a/Commands/InteractionCommands/JoinwatchInteractions.cs b/Commands/InteractionCommands/JoinwatchInteractions.cs index 2bb23bfd..b1ac0fd6 100644 --- a/Commands/InteractionCommands/JoinwatchInteractions.cs +++ b/Commands/InteractionCommands/JoinwatchInteractions.cs @@ -11,82 +11,21 @@ public async Task JoinwatchAdd(InteractionContext ctx, [Option("user", "The user to watch for joins and leaves of.")] DiscordUser user, [Option("note", "An optional note for context.")] string note = "") { - var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); - - if (joinWatchlist.Contains(user.Id)) - { - // User is already watched - - // Get current note; if it's the same, do nothing - var currentNote = await Program.db.HashGetAsync("joinWatchedUsersNotes", user.Id); - if (currentNote == note || (string.IsNullOrWhiteSpace(currentNote) && string.IsNullOrWhiteSpace(note))) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {user.Mention} is already being watched with the same note! Nothing to do."); - return; - } - - // If note is different, update it - - // If new note is empty, remove instead of changing to empty string! - if (string.IsNullOrWhiteSpace(note)) - { - await Program.db.HashDeleteAsync("joinWatchedUsersNotes", user.Id); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully removed the note for {user.Mention}! They are still being watched."); - } - else - { - await Program.db.HashSetAsync("joinWatchedUsersNotes", user.Id, note); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully updated the note for {user.Mention}:\n> {note}"); - } - } - else - { - // User is not joinwatched, watch - await Program.db.ListRightPushAsync("joinWatchedUsers", user.Id); - if (note != "") - await Program.db.HashSetAsync("joinWatchedUsersNotes", user.Id, note); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Now watching for joins/leaves of {user.Mention} to send to the investigations channel" - + (note == "" ? "!" : $" with the following note:\n>>> {note}")); - } + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works; all joinwatches have been converted to notes. Please use `/note add` instead, like this: `/note add user:{user.Id} note:{(string.IsNullOrEmpty(note) ? "" : note)} show_on_join_and_leave:True`"); } [SlashCommand("remove", "Stop watching for joins and leaves of a user.")] public async Task JoinwatchRemove(InteractionContext ctx, [Option("user", "The user to stop watching for joins and leaves of.")] DiscordUser user) { - var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); - - // Check user watch status first; error if not watched - if (!joinWatchlist.Contains(user.Id)) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {user.Mention} is not being watched! Nothing to do."); - return; - } - - Program.db.ListRemove("joinWatchedUsers", joinWatchlist.First(x => x == user.Id)); - await Program.db.HashDeleteAsync("joinWatchedUsersNotes", user.Id); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully unwatched {user.Mention}!"); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works; all joinwatches have been converted to notes. Please use `/note delete` instead, like this: `/note delete user:{user.Id} note:`"); } [SlashCommand("status", "Check the joinwatch status for a user.")] public async Task JoinwatchStatus(InteractionContext ctx, [Option("user", "The user whose joinwatch status to check.")] DiscordUser user) { - var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); - - if (joinWatchlist.Contains(user.Id)) - { - var note = await Program.db.HashGetAsync("joinWatchedUsersNotes", user.Id); - - if (string.IsNullOrWhiteSpace(note)) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Information} {user.Mention} is currently being watched, but no note is set."); - else - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Information} {user.Mention} is currently being watched with the following note:\n> {note}"); - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {user.Mention} is not being watched!"); - } + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works; all joinwatches have been converted to notes. Please use `/note list user:{user.Id}` to show all of this user's notes, or `/note details user:{user.Id} note:` for details on a specific note, instead. Notes with \"Show on Join & Leave\" enabled will behave like joinwatches."); } } } diff --git a/Commands/InteractionCommands/UserNoteInteractions.cs b/Commands/InteractionCommands/UserNoteInteractions.cs index 58bf8627..d39040a1 100644 --- a/Commands/InteractionCommands/UserNoteInteractions.cs +++ b/Commands/InteractionCommands/UserNoteInteractions.cs @@ -15,7 +15,8 @@ public async Task AddUserNoteAsync(InteractionContext ctx, [Option("show_on_modmail", "Whether to show the note when the user opens a modmail thread. Default: true")] bool showOnModmail = true, [Option("show_on_warn", "Whether to show the note when the user is warned. Default: true")] bool showOnWarn = true, [Option("show_all_mods", "Whether to show this note to all mods, versus just yourself. Default: true")] bool showAllMods = true, - [Option("show_once", "Whether to show this note once and then discard it. Default: false")] bool showOnce = false) + [Option("show_once", "Whether to show this note once and then discard it. Default: false")] bool showOnce = false, + [Option("show_on_join_and_leave", "Whether to show this note when the user joins & leaves. Works like joinwatch. Default: false")] bool showOnJoinAndLeave = false) { await ctx.DeferAsync(); @@ -30,6 +31,7 @@ public async Task AddUserNoteAsync(InteractionContext ctx, ShowOnWarn = showOnWarn, ShowAllMods = showAllMods, ShowOnce = showOnce, + ShowOnJoinAndLeave = showOnJoinAndLeave, NoteId = noteId, Timestamp = DateTime.Now, Type = WarningType.Note @@ -88,7 +90,8 @@ public async Task EditUserNoteAsync(InteractionContext ctx, [Option("show_on_modmail", "Whether to show the note when the user opens a modmail thread.")] bool? showOnModmail = null, [Option("show_on_warn", "Whether to show the note when the user is warned.")] bool? showOnWarn = null, [Option("show_all_mods", "Whether to show this note to all mods, versus just yourself.")] bool? showAllMods = null, - [Option("show_once", "Whether to show this note once and then discard it.")] bool? showOnce = null) + [Option("show_once", "Whether to show this note once and then discard it.")] bool? showOnce = null, + [Option("show_on_join_and_leave", "Whether to show this note when the user joins & leaves. Works like joinwatch. Default: false")] bool? showOnJoinAndLeave = false) { // Get note UserNote note; @@ -107,7 +110,7 @@ public async Task EditUserNoteAsync(InteractionContext 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) + if (note.NoteText == newNoteText && showOnModmail is null && showOnWarn is null && showAllMods is null && showOnce is null && showOnJoinAndLeave is null) { await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You didn't change anything about the note!").AsEphemeral()); return; @@ -129,6 +132,8 @@ public async Task EditUserNoteAsync(InteractionContext ctx, showAllMods = note.ShowAllMods; if (showOnce is null) showOnce = note.ShowOnce; + if (showOnJoinAndLeave is null) + showOnJoinAndLeave = note.ShowOnJoinAndLeave; // Assemble new note note.ModUserId = ctx.User.Id; @@ -137,6 +142,7 @@ public async Task EditUserNoteAsync(InteractionContext ctx, note.ShowOnWarn = (bool)showOnWarn; note.ShowAllMods = (bool)showAllMods; note.ShowOnce = (bool)showOnce; + note.ShowOnJoinAndLeave = (bool)showOnJoinAndLeave; note.Type = WarningType.Note; await Program.db.HashSetAsync(user.Id.ToString(), note.NoteId, JsonConvert.SerializeObject(note)); diff --git a/Commands/Lists.cs b/Commands/Lists.cs index fdc7903b..93ecdd63 100644 --- a/Commands/Lists.cs +++ b/Commands/Lists.cs @@ -173,42 +173,7 @@ public async Task JoinWatch( [Description("An optional note for context."), RemainingText] string note = "" ) { - var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); - - if (joinWatchlist.Contains(user.Id)) - { - if (note != "") - { - // User is already joinwatched, just update note - - // Get current note; if it's the same, do nothing - var currentNote = await Program.db.HashGetAsync("joinWatchedUsersNotes", user.Id); - if (currentNote == note || (string.IsNullOrWhiteSpace(currentNote) && string.IsNullOrWhiteSpace(note))) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {user.Mention} is already being watched with the same note! Nothing to do."); - return; - } - - // If note is different, update it - await Program.db.HashSetAsync("joinWatchedUsersNotes", user.Id, note); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully updated the note for {user.Mention} (run again with no note to unwatch):\n> {note}"); - return; - } - - // User is already joinwatched, unwatch - Program.db.ListRemove("joinWatchedUsers", joinWatchlist.First(x => x == user.Id)); - await Program.db.HashDeleteAsync("joinWatchedUsersNotes", user.Id); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully unwatched {user.Mention}, since they were already in the list."); - } - else - { - // User is not joinwatched, watch - await Program.db.ListRightPushAsync("joinWatchedUsers", user.Id); - if (note != "") - await Program.db.HashSetAsync("joinWatchedUsersNotes", user.Id, note); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Now watching for joins/leaves of {user.Mention} to send to the investigations channel" - + (note == "" ? "!" : $" with the following note:\n>>> {note}")); - } + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works; all joinwatches have been converted to notes. To add a note for this user, please use `/note add user:{user.Id} note:{(string.IsNullOrEmpty(note) ? "" : note)} show_on_join_and_leave:True`; to remove one, use `/note delete user:{user.Id} note:`."); } [Command("appealblock")] diff --git a/Events/MemberEvents.cs b/Events/MemberEvents.cs index 77d57768..58418580 100644 --- a/Events/MemberEvents.cs +++ b/Events/MemberEvents.cs @@ -11,7 +11,7 @@ public static async Task GuildMemberAdded(DiscordClient client, GuildMemberAdded if (e.Guild.Id != cfgjson.ServerID) return; - var embed = new DiscordEmbedBuilder() + var userLogEmbed = new DiscordEmbedBuilder() .WithColor(new DiscordColor(0x3E9D28)) .WithTimestamp(DateTimeOffset.Now) .WithThumbnail(e.Member.AvatarUrl) @@ -24,18 +24,20 @@ public static async Task GuildMemberAdded(DiscordClient client, GuildMemberAdded .AddField("Action", "Joined the server", false) .WithFooter($"{client.CurrentUser.Username}JoinEvent"); - LogChannelHelper.LogMessageAsync("users", $"{cfgjson.Emoji.UserJoin} **Member joined the server!** - {e.Member.Id}", embed); + LogChannelHelper.LogMessageAsync("users", $"{cfgjson.Emoji.UserJoin} **Member joined the server!** - {e.Member.Id}", userLogEmbed); - var joinWatchlist = await db.ListRangeAsync("joinWatchedUsers"); + // Get this user's notes that are set to show on join/leave + var userNotes = db.HashGetAll(e.Member.Id.ToString()) + .Where(x => JsonConvert.DeserializeObject(x.Value).Type == WarningType.Note + && JsonConvert.DeserializeObject(x.Value).ShowOnJoinAndLeave).ToDictionary( + x => x.Name.ToString(), + x => JsonConvert.DeserializeObject(x.Value) + ); - if (joinWatchlist.Contains(e.Member.Id)) + if (userNotes.Count > 0) { - if (await db.HashExistsAsync("joinWatchedUsersNotes", e.Member.Id)) - { - embed.AddField($"Joinwatch Note", await db.HashGetAsync("joinWatchedUsersNotes", e.Member.Id)); - } - - LogChannelHelper.LogMessageAsync("investigations", $"{cfgjson.Emoji.Warning} Watched user {e.Member.Mention} just joined the server!", embed); + var notesEmbed = await UserNoteHelpers.GenerateUserNotesEmbedAsync(e.Member, false, userNotes); + LogChannelHelper.LogMessageAsync("investigations", $"{cfgjson.Emoji.Warning} {e.Member.Mention} just joined the server with notes set to show on join!", notesEmbed); } if (db.HashExists("raidmode", e.Guild.Id)) @@ -155,7 +157,7 @@ public static async Task GuildMemberRemoved(DiscordClient client, GuildMemberRem } } - var embed = new DiscordEmbedBuilder() + var userLogEmbed = new DiscordEmbedBuilder() .WithColor(new DiscordColor(0xBA4119)) .WithTimestamp(DateTimeOffset.Now) .WithThumbnail(e.Member.AvatarUrl) @@ -169,18 +171,21 @@ public static async Task GuildMemberRemoved(DiscordClient client, GuildMemberRem .AddField("Roles", rolesStr) .WithFooter($"{client.CurrentUser.Username}LeaveEvent"); - LogChannelHelper.LogMessageAsync("users", $"{cfgjson.Emoji.UserLeave} **Member left the server!** - {e.Member.Id}", embed); - - var joinWatchlist = await db.ListRangeAsync("joinWatchedUsers"); - - if (joinWatchlist.Contains(e.Member.Id)) + LogChannelHelper.LogMessageAsync("users", $"{cfgjson.Emoji.UserLeave} **Member left the server!** - {e.Member.Id}", userLogEmbed); + + // Get this user's notes that are set to show on join/leave + var userNotes = db.HashGetAll(e.Member.Id.ToString()) + .Where(x => JsonConvert.DeserializeObject(x.Value).Type == WarningType.Note + && JsonConvert.DeserializeObject(x.Value).ShowOnJoinAndLeave).ToDictionary( + x => x.Name.ToString(), + x => JsonConvert.DeserializeObject(x.Value) + ); + + DiscordEmbed notesEmbed; + if (userNotes.Count > 0) { - if (await db.HashExistsAsync("joinWatchedUsersNotes", e.Member.Id)) - { - embed.AddField($"Joinwatch Note", await db.HashGetAsync("joinWatchedUsersNotes", e.Member.Id)); - } - - LogChannelHelper.LogMessageAsync("investigations", $"{cfgjson.Emoji.Warning} Watched user {e.Member.Mention} just left the server!", embed); + notesEmbed = await UserNoteHelpers.GenerateUserNotesEmbedAsync(e.Member, false, userNotes); + LogChannelHelper.LogMessageAsync("investigations", $"{cfgjson.Emoji.Warning} {e.Member.Mention} just left the server with notes set to show on leave!", notesEmbed); } } diff --git a/Events/ReadyEvent.cs b/Events/ReadyEvent.cs index 24c3732d..7dc627b5 100644 --- a/Events/ReadyEvent.cs +++ b/Events/ReadyEvent.cs @@ -168,6 +168,15 @@ public static async Task OnStartup(DiscordClient client) discord.Logger.LogError("Heartbeat ping sent: {status} {content}", (int)response.StatusCode, await response.Content.ReadAsStringAsync()); } } + + try + { + await Migrations.JoinwatchMigration.MigrateJoinwatchesToNotesAsync(); + } + catch (Exception ex) + { + client.Logger.LogError(ex, "Failed to migrate joinwatches to notes!"); + } client.Logger.LogInformation(CliptokEventID, "Startup event complete, logged in as {user}", $"{DiscordHelpers.UniqueUsername(client.CurrentUser)}"); } diff --git a/Helpers/UserNoteHelpers.cs b/Helpers/UserNoteHelpers.cs index 2f939d6a..7da45f30 100644 --- a/Helpers/UserNoteHelpers.cs +++ b/Helpers/UserNoteHelpers.cs @@ -120,6 +120,7 @@ await LykosAvatarMethods.UserOrMemberAvatarURL(user, Program.homeGuild, "png") .AddField("Show on Warn", note.ShowOnWarn ? "Yes" : "No", true) .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); diff --git a/Migrations/JoinwatchMigration.cs b/Migrations/JoinwatchMigration.cs new file mode 100644 index 00000000..8fd5cba2 --- /dev/null +++ b/Migrations/JoinwatchMigration.cs @@ -0,0 +1,60 @@ +using static Cliptok.Program; + +namespace Cliptok.Migrations +{ + public class JoinwatchMigration + { + public static async Task MigrateJoinwatchesToNotesAsync() + { + // Migration from joinwatch to user notes + if (!await db.KeyExistsAsync("joinWatchedUsers")) + return; + + var joinWatchedUsersList = await Program.db.ListRangeAsync("joinWatchedUsers"); + var joinWatchNotesList = await Program.db.HashGetAllAsync("joinWatchedUsersNotes"); + int successfulMigrations = 0; + int numJoinWatches = joinWatchedUsersList.Length; + foreach (var user in joinWatchedUsersList) + { + // Get text for note; use joinwatch context if available, or "N/A; imported from joinwatch without context" otherwise + string noteText; + if (joinWatchNotesList.FirstOrDefault(x => x.Name == user) == default) + noteText = "N/A; imported from joinwatch without context"; + else + noteText = joinWatchNotesList.First(x => x.Name == user).Value; + + // Construct note + var note = new UserNote + { + TargetUserId = Convert.ToUInt64(user), + ModUserId = discord.CurrentUser.Id, + NoteText = noteText, + ShowOnModmail = false, + ShowOnWarn = false, + ShowAllMods = false, + ShowOnce = false, + ShowOnJoinAndLeave = true, + NoteId = db.StringIncrement("totalWarnings"), + Timestamp = DateTime.Now, + Type = WarningType.Note + }; + + // Save note & remove joinwatch + await db.HashSetAsync(note.TargetUserId.ToString(), note.NoteId, JsonConvert.SerializeObject(note)); + await db.ListRemoveAsync("joinWatchedUsers", note.TargetUserId); + await db.HashDeleteAsync("joinWatchedUsersNotes", note.TargetUserId); + successfulMigrations++; + } + + if (successfulMigrations > 0) + { + discord.Logger.LogInformation(CliptokEventID, "Successfully migrated {count}/{total} joinwatches to notes.", successfulMigrations, numJoinWatches); + } + + if (numJoinWatches != 0 && successfulMigrations != numJoinWatches) + { + discord.Logger.LogError(CliptokEventID, "Failed to migrate {count} joinwatches to notes!", numJoinWatches - successfulMigrations); + } + } + } +} \ No newline at end of file diff --git a/Structs.cs b/Structs.cs index f2c20af0..f6fc344a 100644 --- a/Structs.cs +++ b/Structs.cs @@ -587,6 +587,9 @@ public class UserNote [JsonProperty("showOnce")] public bool ShowOnce { get; set; } + [JsonProperty("showOnJoinAndLeave")] + public bool ShowOnJoinAndLeave { get; set; } + [JsonProperty("noteId")] public long NoteId { get; set; }