diff --git a/.idea/.idea.catalogger/.idea/sqldialects.xml b/.idea/.idea.catalogger/.idea/sqldialects.xml index 10eef95..4ea96ec 100644 --- a/.idea/.idea.catalogger/.idea/sqldialects.xml +++ b/.idea/.idea.catalogger/.idea/sqldialects.xml @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/Catalogger.Backend/Api/GuildsController.Backup.cs b/Catalogger.Backend/Api/GuildsController.Backup.cs index 4d64a9e..f808b0c 100644 --- a/Catalogger.Backend/Api/GuildsController.Backup.cs +++ b/Catalogger.Backend/Api/GuildsController.Backup.cs @@ -53,7 +53,8 @@ public partial class GuildsController await guildRepository.ImportConfigAsync( guildId.Value, - export.Channels.ToGuildConfig(), + export.Channels.ToChannelConfig(), + export.Channels.ToMessageConfig(), export.BannedSystems, export.KeyRoles ); @@ -91,7 +92,7 @@ public partial class GuildsController return new ConfigExport( config.Id, - ChannelsBackup.FromGuildConfig(config.Channels), + ChannelsBackup.FromGuildConfig(config), config.BannedSystems, config.KeyRoles, invites.Select(i => new InviteExport(i.Code, i.Name)), diff --git a/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs b/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs index 23e424a..7fc43ac 100644 --- a/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs +++ b/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs @@ -29,7 +29,7 @@ public partial class GuildsController var (guildId, _) = await ParseGuildAsync(id); var guildConfig = await guildRepository.GetAsync(guildId); - if (guildConfig.Channels.IgnoredChannels.Contains(channelId)) + if (guildConfig.Messages.IgnoredChannels.Contains(channelId)) return NoContent(); var channel = channelCache @@ -47,8 +47,8 @@ public partial class GuildsController if (channel == null) return NoContent(); - guildConfig.Channels.IgnoredChannels.Add(channelId); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + guildConfig.Messages.IgnoredChannels.Add(channelId); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); return NoContent(); } @@ -59,8 +59,8 @@ public partial class GuildsController var (guildId, _) = await ParseGuildAsync(id); var guildConfig = await guildRepository.GetAsync(guildId); - guildConfig.Channels.IgnoredChannels.Remove(channelId); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + guildConfig.Messages.IgnoredChannels.Remove(channelId); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); return NoContent(); } diff --git a/Catalogger.Backend/Api/GuildsController.Redirects.cs b/Catalogger.Backend/Api/GuildsController.Redirects.cs index 9ce84de..f762896 100644 --- a/Catalogger.Backend/Api/GuildsController.Redirects.cs +++ b/Catalogger.Backend/Api/GuildsController.Redirects.cs @@ -61,7 +61,7 @@ public partial class GuildsController ); guildConfig.Channels.Redirects[source.ID.Value] = target.ID.Value; - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); return NoContent(); } @@ -80,7 +80,7 @@ public partial class GuildsController ); guildConfig.Channels.Redirects.Remove(channelId, out _); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); return NoContent(); } diff --git a/Catalogger.Backend/Api/GuildsController.Users.cs b/Catalogger.Backend/Api/GuildsController.Users.cs index f762d1e..3a3ef43 100644 --- a/Catalogger.Backend/Api/GuildsController.Users.cs +++ b/Catalogger.Backend/Api/GuildsController.Users.cs @@ -37,7 +37,7 @@ public partial class GuildsController var guildConfig = await guildRepository.GetAsync(guildId); var output = new List(); - foreach (var userId in guildConfig.Channels.IgnoredUsers) + foreach (var userId in guildConfig.Messages.IgnoredUsers) { if (cts.Token.IsCancellationRequested) break; @@ -72,11 +72,11 @@ public partial class GuildsController if (user == null) throw new ApiError(HttpStatusCode.NotFound, ErrorCode.BadRequest, "User not found"); - if (guildConfig.Channels.IgnoredUsers.Contains(user.ID.Value)) + if (guildConfig.Messages.IgnoredUsers.Contains(user.ID.Value)) return Ok(new IgnoredUser(user.ID.Value, user.Tag())); - guildConfig.Channels.IgnoredUsers.Add(user.ID.Value); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + guildConfig.Messages.IgnoredUsers.Add(user.ID.Value); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); return Ok(new IgnoredUser(user.ID.Value, user.Tag())); } @@ -87,8 +87,8 @@ public partial class GuildsController var (guildId, _) = await ParseGuildAsync(id); var guildConfig = await guildRepository.GetAsync(guildId); - guildConfig.Channels.IgnoredUsers.Remove(userId); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + guildConfig.Messages.IgnoredUsers.Remove(userId); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); return NoContent(); } diff --git a/Catalogger.Backend/Api/GuildsController.cs b/Catalogger.Backend/Api/GuildsController.cs index f557f90..dfd6479 100644 --- a/Catalogger.Backend/Api/GuildsController.cs +++ b/Catalogger.Backend/Api/GuildsController.cs @@ -159,28 +159,6 @@ public partial class GuildsController( .ToList(); var guildConfig = await guildRepository.GetAsync(guildId); - if (req.IgnoredChannels != null) - { - var categories = channelCache - .GuildChannels(guildId) - .Where(c => c.Type is ChannelType.GuildCategory) - .ToList(); - - if ( - req.IgnoredChannels.Any(cId => - guildChannels.All(c => c.ID.Value != cId) - && categories.All(c => c.ID.Value != cId) - ) - ) - throw new ApiError( - HttpStatusCode.BadRequest, - ErrorCode.BadRequest, - "One or more ignored channels are unknown" - ); - - guildConfig.Channels.IgnoredChannels = req.IgnoredChannels.ToList(); - } - // i love repeating myself wheeeeee if ( req.GuildUpdate == null @@ -334,12 +312,11 @@ public partial class GuildsController( ) guildConfig.Channels.MessageDeleteBulk = req.MessageDeleteBulk ?? 0; - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); return Ok(guildConfig.Channels); } public record ChannelRequest( - ulong[]? IgnoredChannels = null, ulong? GuildUpdate = null, ulong? GuildEmojisUpdate = null, ulong? GuildRoleCreate = null, diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs index b94df0d..ae472e7 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs @@ -261,7 +261,7 @@ public class ChannelCommandsComponents( throw new ArgumentOutOfRangeException(); } - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); goto case "return"; case "return": var (e, c) = ChannelCommands.BuildRootMenu(guildChannels, guild, guildConfig); @@ -384,7 +384,7 @@ public class ChannelCommandsComponents( throw new ArgumentOutOfRangeException(); } - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); List embeds = [ diff --git a/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs b/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs deleted file mode 100644 index 73b0a21..0000000 --- a/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (C) 2021-present sam (starshines.gay) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -using System.ComponentModel; -using Catalogger.Backend.Cache; -using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Repositories; -using Catalogger.Backend.Extensions; -using Catalogger.Backend.Services; -using Remora.Commands.Attributes; -using Remora.Commands.Groups; -using Remora.Discord.API; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.Commands.Attributes; -using Remora.Discord.Commands.Feedback.Services; -using Remora.Discord.Commands.Services; -using Remora.Discord.Extensions.Embeds; -using Remora.Rest.Core; -using IResult = Remora.Results.IResult; - -namespace Catalogger.Backend.Bot.Commands; - -[Group("ignored-channels")] -[Description("Manage channels ignored for logging.")] -[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] -public class IgnoreChannelCommands( - ILogger logger, - GuildRepository guildRepository, - IMemberCache memberCache, - GuildCache guildCache, - ChannelCache channelCache, - PermissionResolverService permissionResolver, - ContextInjectionService contextInjection, - FeedbackService feedbackService -) : CommandGroup -{ - private readonly ILogger _logger = logger.ForContext(); - - [Command("add")] - [Description("Add a channel to the list of ignored channels.")] - public async Task AddIgnoredChannelAsync( - [ChannelTypes( - ChannelType.GuildCategory, - ChannelType.GuildText, - ChannelType.GuildAnnouncement, - ChannelType.GuildForum, - ChannelType.GuildMedia, - ChannelType.GuildVoice, - ChannelType.GuildStageVoice - )] - [Description("The channel to ignore")] - IChannel channel - ) - { - var (_, guildId) = contextInjection.GetUserAndGuild(); - var guildConfig = await guildRepository.GetAsync(guildId); - - if (guildConfig.Channels.IgnoredChannels.Contains(channel.ID.Value)) - return await feedbackService.ReplyAsync( - "That channel is already being ignored.", - isEphemeral: true - ); - - guildConfig.Channels.IgnoredChannels.Add(channel.ID.Value); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); - - return await feedbackService.ReplyAsync( - $"Successfully added {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} to the list of ignored channels." - ); - } - - [Command("remove")] - [Description("Remove a channel from the list of ignored channels.")] - public async Task RemoveIgnoredChannelAsync( - [Description("The channel to stop ignoring")] IChannel channel - ) - { - var (_, guildId) = contextInjection.GetUserAndGuild(); - var guildConfig = await guildRepository.GetAsync(guildId); - - if (!guildConfig.Channels.IgnoredChannels.Contains(channel.ID.Value)) - return await feedbackService.ReplyAsync( - "That channel is already not ignored.", - isEphemeral: true - ); - - guildConfig.Channels.IgnoredChannels.Remove(channel.ID.Value); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); - - return await feedbackService.ReplyAsync( - $"Successfully removed {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} from the list of ignored channels." - ); - } - - [Command("list")] - [Description("List channels ignored for logging.")] - public async Task ListIgnoredChannelsAsync() - { - var (userId, guildId) = contextInjection.GetUserAndGuild(); - if (!guildCache.TryGet(guildId, out var guild)) - throw new CataloggerError("Guild not in cache"); - - var guildChannels = channelCache.GuildChannels(guildId).ToList(); - var guildConfig = await guildRepository.GetAsync(guildId); - - var member = await memberCache.TryGetAsync(guildId, userId); - if (member == null) - throw new CataloggerError("Executing member not found"); - - var ignoredChannels = guildConfig - .Channels.IgnoredChannels.Select(id => - { - var channel = guildChannels.FirstOrDefault(c => c.ID.Value == id); - if (channel == null) - return new IgnoredChannel(IgnoredChannelType.Unknown, DiscordSnowflake.New(id)); - - var type = channel.Type switch - { - ChannelType.GuildCategory => IgnoredChannelType.Category, - _ => IgnoredChannelType.Base, - }; - - return new IgnoredChannel( - type, - channel.ID, - permissionResolver - .GetChannelPermissions(guildId, member, channel) - .HasPermission(DiscordPermission.ViewChannel) - ); - }) - .ToList(); - - var embed = new EmbedBuilder() - .WithTitle($"Ignored channels in {guild.Name}") - .WithColour(DiscordUtils.Purple); - - var nonVisibleCategories = ignoredChannels.Count(c => - c is { Type: IgnoredChannelType.Category, CanSee: false } - ); - var visibleCategories = ignoredChannels - .Where(c => c is { Type: IgnoredChannelType.Category, CanSee: true }) - .ToList(); - - if (nonVisibleCategories != 0 || visibleCategories.Count != 0) - { - var value = string.Join("\n", visibleCategories.Select(c => $"<#{c.Id}>")); - if (nonVisibleCategories != 0) - value += - $"\n\n{nonVisibleCategories} channel(s) are not shown as you do not have access to them."; - - embed.AddField("Categories", value); - } - - var nonVisibleBase = ignoredChannels.Count(c => - c is { Type: IgnoredChannelType.Base, CanSee: false } - ); - var visibleBase = ignoredChannels - .Where(c => c is { Type: IgnoredChannelType.Base, CanSee: true }) - .ToList(); - - if (nonVisibleBase != 0 || visibleBase.Count != 0) - { - var value = string.Join("\n", visibleBase.Select(c => $"<#{c.Id}>")); - if (nonVisibleBase != 0) - value += - $"\n\n{nonVisibleBase} channel(s) are not shown as you do not have access to them."; - - embed.AddField("Channels", value); - } - - var unknownChannels = string.Join( - "\n", - ignoredChannels - .Where(c => c.Type == IgnoredChannelType.Unknown) - .Select(c => $"{c.Id} <#{c.Id}>") - ); - if (!string.IsNullOrWhiteSpace(unknownChannels)) - { - embed.AddField("Unknown", unknownChannels); - } - - return await feedbackService.ReplyAsync(embeds: [embed.Build().GetOrThrow()]); - } - - private record struct IgnoredChannel(IgnoredChannelType Type, Snowflake Id, bool CanSee = true); - - private enum IgnoredChannelType - { - Unknown, - Base, - Category, - } -} diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs new file mode 100644 index 0000000..310142d --- /dev/null +++ b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs @@ -0,0 +1,214 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.ComponentModel; +using Catalogger.Backend.Cache; +using Catalogger.Backend.Cache.InMemoryCache; +using Catalogger.Backend.Database.Repositories; +using Catalogger.Backend.Extensions; +using Catalogger.Backend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Commands.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Rest.Core; +using IResult = Remora.Results.IResult; + +namespace Catalogger.Backend.Bot.Commands; + +[Group("ignore-messages")] +[Description("Manage users, roles, and channels whose messages are not logged.")] +[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] +public partial class IgnoreMessageCommands : CommandGroup +{ + [Group("channels")] + public class Channels( + GuildRepository guildRepository, + IMemberCache memberCache, + GuildCache guildCache, + ChannelCache channelCache, + PermissionResolverService permissionResolver, + ContextInjectionService contextInjection, + FeedbackService feedbackService + ) : CommandGroup + { + [Command("add")] + [Description("Add a channel to the list of ignored channels.")] + [SuppressInteractionResponse(true)] + public async Task AddIgnoredChannelAsync( + [ChannelTypes( + ChannelType.GuildCategory, + ChannelType.GuildText, + ChannelType.GuildAnnouncement, + ChannelType.GuildForum, + ChannelType.GuildMedia, + ChannelType.GuildVoice, + ChannelType.GuildStageVoice + )] + [Description("The channel to ignore")] + IChannel channel + ) + { + var (_, guildId) = contextInjection.GetUserAndGuild(); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (guildConfig.Messages.IgnoredChannels.Contains(channel.ID.Value)) + return await feedbackService.ReplyAsync( + "That channel is already being ignored.", + isEphemeral: true + ); + + guildConfig.Messages.IgnoredChannels.Add(channel.ID.Value); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return await feedbackService.ReplyAsync( + $"Successfully added {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} to the list of ignored channels." + ); + } + + [Command("remove")] + [Description("Remove a channel from the list of ignored channels.")] + public async Task RemoveIgnoredChannelAsync( + [Description("The channel to stop ignoring")] IChannel channel + ) + { + var (_, guildId) = contextInjection.GetUserAndGuild(); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (!guildConfig.Messages.IgnoredChannels.Contains(channel.ID.Value)) + return await feedbackService.ReplyAsync( + "That channel is already not ignored.", + isEphemeral: true + ); + + guildConfig.Messages.IgnoredChannels.Remove(channel.ID.Value); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return await feedbackService.ReplyAsync( + $"Successfully removed {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} from the list of ignored channels." + ); + } + + [Command("list")] + [Description("List channels ignored for logging.")] + public async Task ListIgnoredChannelsAsync() + { + var (userId, guildId) = contextInjection.GetUserAndGuild(); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild not in cache"); + + var guildChannels = channelCache.GuildChannels(guildId).ToList(); + var guildConfig = await guildRepository.GetAsync(guildId); + + var member = await memberCache.TryGetAsync(guildId, userId); + if (member == null) + throw new CataloggerError("Executing member not found"); + + var ignoredChannels = guildConfig + .Messages.IgnoredChannels.Select(id => + { + var channel = guildChannels.FirstOrDefault(c => c.ID.Value == id); + if (channel == null) + return new IgnoredChannel( + IgnoredChannelType.Unknown, + DiscordSnowflake.New(id) + ); + + var type = channel.Type switch + { + ChannelType.GuildCategory => IgnoredChannelType.Category, + _ => IgnoredChannelType.Base, + }; + + return new IgnoredChannel( + type, + channel.ID, + permissionResolver + .GetChannelPermissions(guildId, member, channel) + .HasPermission(DiscordPermission.ViewChannel) + ); + }) + .ToList(); + + var embed = new EmbedBuilder() + .WithTitle($"Ignored channels in {guild.Name}") + .WithColour(DiscordUtils.Purple); + + var nonVisibleCategories = ignoredChannels.Count(c => + c is { Type: IgnoredChannelType.Category, CanSee: false } + ); + var visibleCategories = ignoredChannels + .Where(c => c is { Type: IgnoredChannelType.Category, CanSee: true }) + .ToList(); + + if (nonVisibleCategories != 0 || visibleCategories.Count != 0) + { + var value = string.Join("\n", visibleCategories.Select(c => $"<#{c.Id}>")); + if (nonVisibleCategories != 0) + value += + $"\n\n{nonVisibleCategories} channel(s) are not shown as you do not have access to them."; + + embed.AddField("Categories", value); + } + + var nonVisibleBase = ignoredChannels.Count(c => + c is { Type: IgnoredChannelType.Base, CanSee: false } + ); + var visibleBase = ignoredChannels + .Where(c => c is { Type: IgnoredChannelType.Base, CanSee: true }) + .ToList(); + + if (nonVisibleBase != 0 || visibleBase.Count != 0) + { + var value = string.Join("\n", visibleBase.Select(c => $"<#{c.Id}>")); + if (nonVisibleBase != 0) + value += + $"\n\n{nonVisibleBase} channel(s) are not shown as you do not have access to them."; + + embed.AddField("Channels", value); + } + + var unknownChannels = string.Join( + "\n", + ignoredChannels + .Where(c => c.Type == IgnoredChannelType.Unknown) + .Select(c => $"{c.Id} <#{c.Id}>") + ); + if (!string.IsNullOrWhiteSpace(unknownChannels)) + { + embed.AddField("Unknown", unknownChannels); + } + + return await feedbackService.ReplyAsync(embeds: [embed.Build().GetOrThrow()]); + } + + private record struct IgnoredChannel( + IgnoredChannelType Type, + Snowflake Id, + bool CanSee = true + ); + + private enum IgnoredChannelType + { + Unknown, + Base, + Category, + } + } +} diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs new file mode 100644 index 0000000..6515f47 --- /dev/null +++ b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs @@ -0,0 +1,122 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.ComponentModel; +using Catalogger.Backend.Cache.InMemoryCache; +using Catalogger.Backend.Database.Repositories; +using Catalogger.Backend.Extensions; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Commands.Services; +using Remora.Discord.Extensions.Embeds; +using IResult = Remora.Results.IResult; + +namespace Catalogger.Backend.Bot.Commands; + +public partial class IgnoreMessageCommands +{ + [Group("roles")] + public class Roles( + GuildRepository guildRepository, + GuildCache guildCache, + RoleCache roleCache, + ContextInjectionService contextInjection, + FeedbackService feedbackService + ) : CommandGroup + { + [Command("add")] + [Description("Add a role to the list of ignored roles.")] + public async Task AddIgnoredRoleAsync( + [Description("The role to ignore")] IRole role + ) + { + var (_, guildId) = contextInjection.GetUserAndGuild(); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (guildConfig.Messages.IgnoredRoles.Contains(role.ID.Value)) + return await feedbackService.ReplyAsync( + "That role is already being ignored.", + isEphemeral: true + ); + + guildConfig.Messages.IgnoredRoles.Add(role.ID.Value); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return await feedbackService.ReplyAsync( + $"Successfully added {role.Name} to the list of ignored roles." + ); + } + + [Command("remove")] + [Description("Remove a role from the list of ignored roles.")] + public async Task RemoveIgnoredRoleAsync( + [Description("The role to stop ignoring")] IRole role + ) + { + var (_, guildId) = contextInjection.GetUserAndGuild(); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (!guildConfig.Messages.IgnoredRoles.Contains(role.ID.Value)) + return await feedbackService.ReplyAsync( + "That role is already not ignored.", + isEphemeral: true + ); + + guildConfig.Messages.IgnoredRoles.Remove(role.ID.Value); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return await feedbackService.ReplyAsync( + $"Successfully removed {role.Name} from the list of ignored roles." + ); + } + + [Command("list")] + [Description("List roles ignored for logging.")] + public async Task ListIgnoredRolesAsync() + { + var (_, guildId) = contextInjection.GetUserAndGuild(); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild not in cache"); + + var guildConfig = await guildRepository.GetAsync(guildId); + + var roles = roleCache + .GuildRoles(guildId) + .Where(r => guildConfig.Messages.IgnoredRoles.Contains(r.ID.Value)) + .OrderByDescending(r => r.Position) + .Select(r => $"<@&{r.ID}>") + .ToList(); + if (roles.Count == 0) + return await feedbackService.ReplyAsync( + "No roles are being ignored right now.", + isEphemeral: true + ); + + return await feedbackService.ReplyAsync( + embeds: + [ + new EmbedBuilder() + .WithTitle($"Ignored roles in {guild.Name}") + .WithDescription(string.Join("\n", roles)) + .WithColour(DiscordUtils.Purple) + .Build() + .GetOrThrow(), + ] + ); + } + } +} diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs new file mode 100644 index 0000000..89f329b --- /dev/null +++ b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs @@ -0,0 +1,124 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.ComponentModel; +using Catalogger.Backend.Cache; +using Catalogger.Backend.Cache.InMemoryCache; +using Catalogger.Backend.Database.Repositories; +using Catalogger.Backend.Extensions; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Commands.Services; +using Remora.Discord.Pagination.Extensions; +using Remora.Rest.Core; +using IResult = Remora.Results.IResult; + +namespace Catalogger.Backend.Bot.Commands; + +public partial class IgnoreMessageCommands +{ + [Group("users")] + public class Users( + GuildRepository guildRepository, + IMemberCache memberCache, + GuildCache guildCache, + UserCache userCache, + ContextInjectionService contextInjection, + FeedbackService feedbackService + ) : CommandGroup + { + [Command("add")] + [Description("Add a user to the list of ignored users.")] + public async Task AddIgnoredUserAsync( + [Description("The user to ignore")] IUser user + ) + { + var (_, guildId) = contextInjection.GetUserAndGuild(); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (guildConfig.Messages.IgnoredUsers.Contains(user.ID.Value)) + return await feedbackService.ReplyAsync( + "That user is already being ignored.", + isEphemeral: true + ); + + guildConfig.Messages.IgnoredUsers.Add(user.ID.Value); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return await feedbackService.ReplyAsync( + $"Successfully added {user.PrettyFormat()} to the list of ignored users." + ); + } + + [Command("remove")] + [Description("Remove a user from the list of ignored users.")] + public async Task RemoveIgnoredUserAsync( + [Description("The user to stop ignoring")] IUser user + ) + { + var (_, guildId) = contextInjection.GetUserAndGuild(); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (!guildConfig.Messages.IgnoredUsers.Contains(user.ID.Value)) + return await feedbackService.ReplyAsync( + "That user is already not ignored.", + isEphemeral: true + ); + + guildConfig.Messages.IgnoredUsers.Remove(user.ID.Value); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return await feedbackService.ReplyAsync( + $"Successfully removed {user.PrettyFormat()} from the list of ignored users." + ); + } + + [Command("list")] + [Description("List currently ignored users.")] + public async Task ListIgnoredUsersAsync() + { + var (userId, guildId) = contextInjection.GetUserAndGuild(); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild was not cached"); + + var guildConfig = await guildRepository.GetAsync(guildId); + + if (guildConfig.Messages.IgnoredUsers.Count == 0) + return await feedbackService.ReplyAsync("No users are being ignored right now."); + + var users = new List(); + foreach (var id in guildConfig.Messages.IgnoredUsers) + { + var user = await TryGetUserAsync(guildId, DiscordSnowflake.New(id)); + users.Add(user?.PrettyFormat() ?? $"*(unknown user {id})* <@{id}>"); + } + + return await feedbackService.SendContextualPaginatedMessageAsync( + userId, + DiscordUtils.PaginateStrings( + users, + $"Ignored users for {guild.Name} ({users.Count})" + ) + ); + } + + private async Task TryGetUserAsync(Snowflake guildId, Snowflake userId) => + (await memberCache.TryGetAsync(guildId, userId))?.User.Value + ?? await userCache.GetUserAsync(userId); + } +} diff --git a/Catalogger.Backend/Bot/Commands/IgnoreUserCommands.cs b/Catalogger.Backend/Bot/Commands/IgnoreUserCommands.cs deleted file mode 100644 index 2474031..0000000 --- a/Catalogger.Backend/Bot/Commands/IgnoreUserCommands.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (C) 2021-present sam (starshines.gay) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -using System.ComponentModel; -using Catalogger.Backend.Cache; -using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database.Repositories; -using Catalogger.Backend.Extensions; -using Remora.Commands.Attributes; -using Remora.Commands.Groups; -using Remora.Discord.API; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.Commands.Feedback.Services; -using Remora.Discord.Commands.Services; -using Remora.Discord.Pagination.Extensions; -using Remora.Rest.Core; -using IResult = Remora.Results.IResult; - -namespace Catalogger.Backend.Bot.Commands; - -[Group("ignored-users")] -[Description("Manage users ignored for logging.")] -public class IgnoreUserCommands( - GuildRepository guildRepository, - GuildCache guildCache, - IMemberCache memberCache, - UserCache userCache, - ContextInjectionService contextInjection, - FeedbackService feedbackService -) : CommandGroup -{ - [Command("add")] - [Description("Add a user to the list of ignored users.")] - public async Task AddIgnoredUserAsync([Description("The user to ignore")] IUser user) - { - var (_, guildId) = contextInjection.GetUserAndGuild(); - var guildConfig = await guildRepository.GetAsync(guildId); - - if (guildConfig.Channels.IgnoredUsers.Contains(user.ID.Value)) - return await feedbackService.ReplyAsync( - "That user is already being ignored.", - isEphemeral: true - ); - - guildConfig.Channels.IgnoredUsers.Add(user.ID.Value); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); - - return await feedbackService.ReplyAsync( - $"Successfully added {user.PrettyFormat()} to the list of ignored users." - ); - } - - [Command("remove")] - [Description("Remove a user from the list of ignored users.")] - public async Task RemoveIgnoredUserAsync( - [Description("The user to stop ignoring")] IUser user - ) - { - var (_, guildId) = contextInjection.GetUserAndGuild(); - var guildConfig = await guildRepository.GetAsync(guildId); - - if (!guildConfig.Channels.IgnoredUsers.Contains(user.ID.Value)) - return await feedbackService.ReplyAsync( - "That user is already not ignored.", - isEphemeral: true - ); - - guildConfig.Channels.IgnoredUsers.Remove(user.ID.Value); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); - - return await feedbackService.ReplyAsync( - $"Successfully removed {user.PrettyFormat()} from the list of ignored users." - ); - } - - [Command("list")] - [Description("List currently ignored users.")] - public async Task ListIgnoredUsersAsync() - { - var (userId, guildId) = contextInjection.GetUserAndGuild(); - if (!guildCache.TryGet(guildId, out var guild)) - throw new CataloggerError("Guild was not cached"); - - var guildConfig = await guildRepository.GetAsync(guildId); - - if (guildConfig.Channels.IgnoredUsers.Count == 0) - return await feedbackService.ReplyAsync("No users are being ignored right now."); - - var users = new List(); - foreach (var id in guildConfig.Channels.IgnoredUsers) - { - var user = await TryGetUserAsync(guildId, DiscordSnowflake.New(id)); - users.Add(user?.PrettyFormat() ?? $"*(unknown user {id})* <@{id}>"); - } - - return await feedbackService.SendContextualPaginatedMessageAsync( - userId, - DiscordUtils.PaginateStrings(users, $"Ignored users for {guild.Name} ({users.Count})") - ); - } - - private async Task TryGetUserAsync(Snowflake guildId, Snowflake userId) => - (await memberCache.TryGetAsync(guildId, userId))?.User.Value - ?? await userCache.GetUserAsync(userId); -} diff --git a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs index c776675..0fb2a3d 100644 --- a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs +++ b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs @@ -61,7 +61,7 @@ public class RedirectCommands( var (_, guildId) = contextInjectionService.GetUserAndGuild(); var guildConfig = await guildRepository.GetAsync(guildId); guildConfig.Channels.Redirects[source.ID.Value] = target.ID.Value; - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); var output = $"Success! Edited and deleted messages from {FormatChannel(source)} will now be redirected to <#{target.ID}>."; @@ -101,7 +101,7 @@ public class RedirectCommands( var guildConfig = await guildRepository.GetAsync(guildId); var wasSet = guildConfig.Channels.Redirects.Remove(source.ID.Value); - await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); var output = wasSet ? $"Removed the redirect for {FormatChannel(source)}! Message logs from" diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs index 93666fe..935734a 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs @@ -17,6 +17,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; +using Microsoft.Extensions.Logging.Configuration; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.Extensions.Embeds; @@ -97,8 +98,11 @@ public class ChannelCreateResponder( var guildConfig = await guildRepository.GetAsync(ch.GuildID); webhookExecutor.QueueLog( - guildConfig, - LogChannelType.ChannelCreate, + webhookExecutor.GetLogChannel( + guildConfig, + LogChannelType.ChannelCreate, + channelId: ch.ID + ), builder.Build().GetOrThrow() ); return Result.Success; diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs index aaee939..7e13bcb 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs @@ -68,8 +68,11 @@ public class ChannelDeleteResponder( embed.AddField("Description", topic); webhookExecutor.QueueLog( - guildConfig, - LogChannelType.ChannelDelete, + webhookExecutor.GetLogChannel( + guildConfig, + LogChannelType.ChannelDelete, + channelId: channel.ID + ), embed.Build().GetOrThrow() ); return Result.Success; diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs index 56ca415..7ed3954 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs @@ -180,16 +180,14 @@ public class ChannelUpdateResponder( if (builder.Fields.Count == 0) return Result.Success; - var logChannel = webhookExecutor.GetLogChannel( - guildConfig, - LogChannelType.ChannelUpdate, - channelId: evt.ID, - userId: null + webhookExecutor.QueueLog( + webhookExecutor.GetLogChannel( + guildConfig, + LogChannelType.ChannelUpdate, + channelId: evt.ID + ), + builder.Build().GetOrThrow() ); - if (logChannel == null) - return Result.Success; - - webhookExecutor.QueueLog(logChannel.Value, builder.Build().GetOrThrow()); return Result.Success; } diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs index 6cdfe86..e02b790 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs @@ -315,19 +315,20 @@ public class GuildMemberUpdateResponder( .WithFooter($"User ID: {member.User.ID}") .WithCurrentTimestamp(); - var addedRoles = member.Roles.Except(oldRoles).Select(s => s.Value).ToList(); - var removedRoles = oldRoles.Except(member.Roles).Select(s => s.Value).ToList(); + var addedRoles = member.Roles.Except(oldRoles).ToList(); + var removedRoles = oldRoles.Except(member.Roles).ToList(); if (addedRoles.Count != 0) { roleUpdate.AddField("Added", string.Join(", ", addedRoles.Select(id => $"<@&{id}>"))); // Add all added key roles to the log - if (!addedRoles.Except(guildConfig.KeyRoles).Any()) + if (!addedRoles.Select(s => s.Value).Except(guildConfig.KeyRoles).Any()) { var value = string.Join( "\n", addedRoles + .Select(s => s.Value) .Where(guildConfig.KeyRoles.Contains) .Select(id => { @@ -348,11 +349,12 @@ public class GuildMemberUpdateResponder( ); // Add all removed key roles to the log - if (!removedRoles.Except(guildConfig.KeyRoles).Any()) + if (!removedRoles.Select(s => s.Value).Except(guildConfig.KeyRoles).Any()) { var value = string.Join( "\n", removedRoles + .Select(s => s.Value) .Where(guildConfig.KeyRoles.Contains) .Select(id => { @@ -369,8 +371,12 @@ public class GuildMemberUpdateResponder( if (roleUpdate.Fields.Count != 0) { webhookExecutor.QueueLog( - guildConfig, - LogChannelType.GuildMemberUpdate, + webhookExecutor.GetLogChannel( + guildConfig, + LogChannelType.GuildMemberUpdate, + // Check for all added and removed roles + roleIds: addedRoles.Concat(removedRoles).ToList() + ), roleUpdate.Build().GetOrThrow() ); } diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs index 3eebf2f..4f2b3a7 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs @@ -53,7 +53,13 @@ public class MessageCreateResponder( var guild = await guildRepository.GetAsync(msg.GuildID); // The guild needs to have enabled at least one of the message logging events, // and the channel must not be ignored, to store the message. - if (guild.IsMessageIgnored(msg.ChannelID, msg.Author.ID)) + if ( + guild.IsMessageIgnored( + msg.ChannelID, + msg.Author.ID, + msg.Member.OrDefault()?.Roles.OrDefault() + ) + ) { await messageRepository.IgnoreMessageAsync(msg.ID.Value); return Result.Success; diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs index 6676839..46fdd0b 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs @@ -42,7 +42,7 @@ public class MessageDeleteBulkResponder( public async Task RespondAsync(IMessageDeleteBulk evt, CancellationToken ct = default) { var guild = await guildRepository.GetAsync(evt.GuildID); - if (guild.IsMessageIgnored(evt.ChannelID, null)) + if (guild.IsMessageIgnored(evt.ChannelID, null, null)) return Result.Success; var logChannel = webhookExecutor.GetLogChannel( diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs index f866b94..9cf621e 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs @@ -64,22 +64,15 @@ public class MessageDeleteResponder( return Result.Success; var guild = await guildRepository.GetAsync(evt.GuildID); - if (guild.IsMessageIgnored(evt.ChannelID, evt.ID)) + if (guild.IsMessageIgnored(evt.ChannelID, null, null)) return Result.Success; - var logChannel = webhookExecutor.GetLogChannel( - guild, - LogChannelType.MessageDelete, - evt.ChannelID - ); var msg = await messageRepository.GetMessageAsync(evt.ID.Value, ct); // Sometimes a message that *should* be logged isn't stored in the database, notify the user of that if (msg == null) { - if (logChannel == null) - return Result.Success; webhookExecutor.QueueLog( - logChannel.Value, + webhookExecutor.GetLogChannel(guild, LogChannelType.MessageDelete, evt.ChannelID), new Embed( Title: "Message deleted", Description: $"A message not found in the database was deleted in <#{evt.ChannelID}> ({evt.ChannelID}).", @@ -107,7 +100,7 @@ public class MessageDeleteResponder( } } - logChannel = webhookExecutor.GetLogChannel( + var logChannel = webhookExecutor.GetLogChannel( guild, LogChannelType.MessageDelete, evt.ChannelID, @@ -173,7 +166,7 @@ public class MessageDeleteResponder( builder.AddField("Attachments", attachmentInfo, false); } - webhookExecutor.QueueLog(logChannel.Value, builder.Build().GetOrThrow()); + webhookExecutor.QueueLog(logChannel, builder.Build().GetOrThrow()); return Result.Success; } } diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs index 4df76ea..0d09658 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs @@ -54,8 +54,11 @@ public class RoleCreateResponder( } webhookExecutor.QueueLog( - guildConfig, - LogChannelType.GuildRoleCreate, + webhookExecutor.GetLogChannel( + guildConfig, + LogChannelType.GuildRoleCreate, + roleId: evt.Role.ID + ), embed.Build().GetOrThrow() ); diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs index 8566434..f76b339 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs @@ -70,8 +70,11 @@ public class RoleDeleteResponder( } webhookExecutor.QueueLog( - guildConfig, - LogChannelType.GuildRoleDelete, + webhookExecutor.GetLogChannel( + guildConfig, + LogChannelType.GuildRoleDelete, + roleId: role.ID + ), embed.Build().GetOrThrow() ); } diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs index 828ef22..4ed8c2f 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs @@ -96,8 +96,11 @@ public class RoleUpdateResponder( var guildConfig = await guildRepository.GetAsync(evt.GuildID); webhookExecutor.QueueLog( - guildConfig, - LogChannelType.GuildRoleUpdate, + webhookExecutor.GetLogChannel( + guildConfig, + LogChannelType.GuildRoleUpdate, + roleId: evt.Role.ID + ), embed.Build().GetOrThrow() ); } diff --git a/Catalogger.Backend/Database/DatabasePool.cs b/Catalogger.Backend/Database/DatabasePool.cs index 3677ae3..9b1712f 100644 --- a/Catalogger.Backend/Database/DatabasePool.cs +++ b/Catalogger.Backend/Database/DatabasePool.cs @@ -116,6 +116,7 @@ public class DatabasePool SqlMapper.AddTypeHandler(new PassthroughTypeHandler()); SqlMapper.AddTypeHandler(new JsonTypeHandler()); + SqlMapper.AddTypeHandler(new JsonTypeHandler()); } // Copied from PluralKit: diff --git a/Catalogger.Backend/Database/Migrations/004_split_message_config.down.sql b/Catalogger.Backend/Database/Migrations/004_split_message_config.down.sql new file mode 100644 index 0000000..a9f1de1 --- /dev/null +++ b/Catalogger.Backend/Database/Migrations/004_split_message_config.down.sql @@ -0,0 +1,3 @@ +update guilds set channels = (channels || messages) - 'IgnoredRoles'; + +alter table guilds drop column messages; diff --git a/Catalogger.Backend/Database/Migrations/004_split_message_config.up.sql b/Catalogger.Backend/Database/Migrations/004_split_message_config.up.sql new file mode 100644 index 0000000..6cd35f0 --- /dev/null +++ b/Catalogger.Backend/Database/Migrations/004_split_message_config.up.sql @@ -0,0 +1,12 @@ +alter table guilds + add column messages jsonb not null default '{}'; + +-- Extract the current message-related configuration options into the new "messages" column +-- noinspection SqlWithoutWhere +update guilds +set messages = jsonb_build_object('IgnoredUsers', channels['IgnoredUsers'], 'IgnoredChannels', + channels['IgnoredChannels'], 'IgnoredUsersPerChannel', + channels['IgnoredUsersPerChannel']); + +-- We don't update the "channels" column as it will be cleared out automatically over time, +-- as channel configurations are updated by the bot diff --git a/Catalogger.Backend/Database/Models/ConfigExport.cs b/Catalogger.Backend/Database/Models/ConfigExport.cs index b6514f7..664bfa8 100644 --- a/Catalogger.Backend/Database/Models/ConfigExport.cs +++ b/Catalogger.Backend/Database/Models/ConfigExport.cs @@ -19,6 +19,7 @@ public class ChannelsBackup { public List IgnoredChannels { get; init; } = []; public List IgnoredUsers { get; init; } = []; + public List IgnoredRoles { get; init; } = []; public Dictionary> IgnoredUsersPerChannel { get; init; } = []; public Dictionary Redirects { get; init; } = []; @@ -46,12 +47,18 @@ public class ChannelsBackup public ulong MessageDelete { get; init; } public ulong MessageDeleteBulk { get; init; } - public Guild.ChannelConfig ToGuildConfig() => + public Guild.MessageConfig ToMessageConfig() => new() { IgnoredChannels = IgnoredChannels, IgnoredUsers = IgnoredUsers, + IgnoredRoles = IgnoredRoles, IgnoredUsersPerChannel = IgnoredUsersPerChannel, + }; + + public Guild.ChannelConfig ToChannelConfig() => + new() + { Redirects = Redirects, GuildUpdate = GuildUpdate, GuildEmojisUpdate = GuildEmojisUpdate, @@ -78,35 +85,36 @@ public class ChannelsBackup MessageDeleteBulk = MessageDeleteBulk, }; - public static ChannelsBackup FromGuildConfig(Guild.ChannelConfig channels) => + public static ChannelsBackup FromGuildConfig(Guild guild) => new() { - IgnoredChannels = channels.IgnoredChannels, - IgnoredUsers = channels.IgnoredUsers, - IgnoredUsersPerChannel = channels.IgnoredUsersPerChannel, - Redirects = channels.Redirects, - GuildUpdate = channels.GuildUpdate, - GuildEmojisUpdate = channels.GuildEmojisUpdate, - GuildRoleCreate = channels.GuildRoleCreate, - GuildRoleUpdate = channels.GuildRoleUpdate, - GuildRoleDelete = channels.GuildRoleDelete, - ChannelCreate = channels.ChannelCreate, - ChannelUpdate = channels.ChannelUpdate, - ChannelDelete = channels.ChannelDelete, - GuildMemberAdd = channels.GuildMemberAdd, - GuildMemberUpdate = channels.GuildMemberUpdate, - GuildKeyRoleUpdate = channels.GuildKeyRoleUpdate, - GuildMemberNickUpdate = channels.GuildMemberNickUpdate, - GuildMemberAvatarUpdate = channels.GuildMemberAvatarUpdate, - GuildMemberTimeout = channels.GuildMemberTimeout, - GuildMemberRemove = channels.GuildMemberRemove, - GuildMemberKick = channels.GuildMemberKick, - GuildBanAdd = channels.GuildBanAdd, - GuildBanRemove = channels.GuildBanRemove, - InviteCreate = channels.InviteCreate, - InviteDelete = channels.InviteDelete, - MessageUpdate = channels.MessageUpdate, - MessageDelete = channels.MessageDelete, - MessageDeleteBulk = channels.MessageDeleteBulk, + IgnoredChannels = guild.Messages.IgnoredChannels, + IgnoredUsers = guild.Messages.IgnoredUsers, + IgnoredRoles = guild.Messages.IgnoredRoles, + IgnoredUsersPerChannel = guild.Messages.IgnoredUsersPerChannel, + Redirects = guild.Channels.Redirects, + GuildUpdate = guild.Channels.GuildUpdate, + GuildEmojisUpdate = guild.Channels.GuildEmojisUpdate, + GuildRoleCreate = guild.Channels.GuildRoleCreate, + GuildRoleUpdate = guild.Channels.GuildRoleUpdate, + GuildRoleDelete = guild.Channels.GuildRoleDelete, + ChannelCreate = guild.Channels.ChannelCreate, + ChannelUpdate = guild.Channels.ChannelUpdate, + ChannelDelete = guild.Channels.ChannelDelete, + GuildMemberAdd = guild.Channels.GuildMemberAdd, + GuildMemberUpdate = guild.Channels.GuildMemberUpdate, + GuildKeyRoleUpdate = guild.Channels.GuildKeyRoleUpdate, + GuildMemberNickUpdate = guild.Channels.GuildMemberNickUpdate, + GuildMemberAvatarUpdate = guild.Channels.GuildMemberAvatarUpdate, + GuildMemberTimeout = guild.Channels.GuildMemberTimeout, + GuildMemberRemove = guild.Channels.GuildMemberRemove, + GuildMemberKick = guild.Channels.GuildMemberKick, + GuildBanAdd = guild.Channels.GuildBanAdd, + GuildBanRemove = guild.Channels.GuildBanRemove, + InviteCreate = guild.Channels.InviteCreate, + InviteDelete = guild.Channels.InviteDelete, + MessageUpdate = guild.Channels.MessageUpdate, + MessageDelete = guild.Channels.MessageDelete, + MessageDeleteBulk = guild.Channels.MessageDeleteBulk, }; } diff --git a/Catalogger.Backend/Database/Models/Guild.cs b/Catalogger.Backend/Database/Models/Guild.cs index afbd1cb..6ec6d82 100644 --- a/Catalogger.Backend/Database/Models/Guild.cs +++ b/Catalogger.Backend/Database/Models/Guild.cs @@ -24,18 +24,28 @@ public class Guild public required ulong Id { get; init; } public ChannelConfig Channels { get; init; } = new(); + public MessageConfig Messages { get; init; } = new(); public string[] BannedSystems { get; set; } = []; public ulong[] KeyRoles { get; set; } = []; + // These channels and roles are ignored for channel/role update/delete events. + public ulong[] IgnoredChannels { get; set; } = []; + public ulong[] IgnoredRoles { get; set; } = []; + public bool IsSystemBanned(PluralkitApiService.PkSystem system) => BannedSystems.Contains(system.Id) || BannedSystems.Contains(system.Uuid.ToString()); - public bool IsMessageIgnored(Snowflake channelId, Snowflake? userId) + public bool IsMessageIgnored( + Snowflake channelId, + Snowflake? userId, + IReadOnlyList? roleIds + ) { if ( Channels is { MessageDelete: 0, MessageUpdate: 0, MessageDeleteBulk: 0 } - || Channels.IgnoredChannels.Contains(channelId.ToUlong()) - || (userId != null && Channels.IgnoredUsers.Contains(userId.Value.ToUlong())) + || Messages.IgnoredChannels.Contains(channelId.ToUlong()) + || (userId != null && Messages.IgnoredUsers.Contains(userId.Value.ToUlong())) + || (roleIds != null && roleIds.Any(r => Messages.IgnoredRoles.Any(id => r.Value == id))) ) return true; @@ -43,7 +53,7 @@ public class Guild return false; if ( - Channels.IgnoredUsersPerChannel.TryGetValue( + Messages.IgnoredUsersPerChannel.TryGetValue( channelId.ToUlong(), out var thisChannelIgnoredUsers ) @@ -53,11 +63,16 @@ public class Guild return false; } - public class ChannelConfig + public class MessageConfig { public List IgnoredChannels { get; set; } = []; + public List IgnoredRoles { get; set; } = []; public List IgnoredUsers { get; init; } = []; public Dictionary> IgnoredUsersPerChannel { get; init; } = []; + } + + public class ChannelConfig + { public Dictionary Redirects { get; init; } = []; public ulong GuildUpdate { get; set; } diff --git a/Catalogger.Backend/Database/Repositories/GuildRepository.cs b/Catalogger.Backend/Database/Repositories/GuildRepository.cs index 2a95403..276b89a 100644 --- a/Catalogger.Backend/Database/Repositories/GuildRepository.cs +++ b/Catalogger.Backend/Database/Repositories/GuildRepository.cs @@ -131,24 +131,31 @@ public class GuildRepository(ILogger logger, DatabaseConnection conn) new { GuildId = guildId.Value, RoleId = roleId.Value } ); - public async Task UpdateChannelConfigAsync(Snowflake id, Guild.ChannelConfig config) => + public async Task UpdateChannelConfigAsync(Snowflake id, Guild config) => await conn.ExecuteAsync( - "update guilds set channels = @Channels::jsonb where id = @Id", - new { Id = id.Value, Channels = config } + "update guilds set channels = @Channels::jsonb, messages = @Messages::jsonb where id = @Id", + new + { + Id = id.Value, + config.Channels, + config.Messages, + } ); public async Task ImportConfigAsync( ulong id, Guild.ChannelConfig channels, + Guild.MessageConfig messages, string[] bannedSystems, ulong[] keyRoles ) => await conn.ExecuteAsync( - "update guilds set channels = @channels::jsonb, banned_systems = @bannedSystems, key_roles = @keyRoles where id = @id", + "update guilds set channels = @channels::jsonb, messages = @messages::jsonb, banned_systems = @bannedSystems, key_roles = @keyRoles where id = @id", new { id, channels, + messages, bannedSystems, keyRoles, } diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index ed9292a..f69c42e 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -27,6 +27,7 @@ using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Gateway.Commands; using Remora.Discord.API.Objects; using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Responders; using Remora.Discord.Extensions.Extensions; using Remora.Discord.Gateway; using Remora.Discord.Interactivity.Extensions; @@ -83,6 +84,7 @@ builder ] ); }) + .Configure(opts => opts.SuppressAutomaticResponses = true) .AddDiscordCommands( enableSlash: true, useDefaultCommandResponder: false, @@ -94,10 +96,12 @@ builder .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() - .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() - .WithCommandGroup() // End command tree .Finish() .AddPagination() diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index df2a122..221b4a1 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -60,7 +60,14 @@ public class WebhookExecutorService( /// public void QueueLog(Guild guildConfig, LogChannelType logChannelType, IEmbed embed) { - var logChannel = GetLogChannel(guildConfig, logChannelType, channelId: null, userId: null); + var logChannel = GetLogChannel( + guildConfig, + logChannelType, + channelId: null, + userId: null, + roleId: null, + roleIds: null + ); if (logChannel == null) return; @@ -70,16 +77,16 @@ public class WebhookExecutorService( /// /// Queues a log embed for the given channel ID. /// - public void QueueLog(ulong channelId, IEmbed embed) + public void QueueLog(ulong? channelId, IEmbed embed) { - if (channelId == 0) + if (channelId is null or 0) return; - var queue = _cache.GetOrAdd(channelId, []); + var queue = _cache.GetOrAdd(channelId.Value, []); queue.Enqueue(embed); - _cache[channelId] = queue; + _cache[channelId.Value] = queue; - SetTimer(channelId, queue); + SetTimer(channelId.Value, queue); } /// @@ -251,14 +258,72 @@ public class WebhookExecutorService( } public ulong? GetLogChannel( + Guild guild, + LogChannelType logChannelType, + Snowflake? channelId = null, + ulong? userId = null, + Snowflake? roleId = null, + IReadOnlyList? roleIds = null + ) + { + var isMessageLog = + logChannelType + is LogChannelType.MessageUpdate + or LogChannelType.MessageDelete + or LogChannelType.MessageDeleteBulk; + + // Check if we're getting the channel for a channel log + var isChannelLog = + channelId != null + && logChannelType + is LogChannelType.ChannelCreate + or LogChannelType.ChannelDelete + or LogChannelType.ChannelUpdate; + + // Check if we're getting the channel for a role log + var isRoleLog = + roleId != null + && logChannelType + is LogChannelType.GuildRoleCreate + or LogChannelType.GuildRoleUpdate + or LogChannelType.GuildRoleDelete; + + // Check if we're getting the channel for a member update log + var isMemberRoleUpdateLog = + roleIds != null && logChannelType is LogChannelType.GuildMemberUpdate; + + if (isMessageLog) + return GetMessageLogChannel(guild, logChannelType, channelId, userId); + + if (isChannelLog && guild.IgnoredChannels.Contains(channelId!.Value.Value)) + return null; + + if (isRoleLog && guild.IgnoredRoles.Contains(roleId!.Value.Value)) + return null; + + // Member update logs are only ignored if *all* updated roles are ignored + if (isMemberRoleUpdateLog && roleIds!.All(r => guild.IgnoredRoles.Contains(r.Value))) + return null; + + // If nothing is ignored, return the correct log channel! + return GetDefaultLogChannel(guild, logChannelType); + } + + private ulong? GetMessageLogChannel( Guild guild, LogChannelType logChannelType, Snowflake? channelId = null, ulong? userId = null ) { + // Check if the user is ignored globally + if (userId != null && guild.Messages.IgnoredUsers.Contains(userId.Value)) + return null; + + // If the user isn't ignored and we didn't get a channel ID, return the default log channel if (channelId == null) return GetDefaultLogChannel(guild, logChannelType); + if (!channelCache.TryGet(channelId.Value, out var channel)) return null; @@ -282,25 +347,23 @@ public class WebhookExecutorService( categoryId = channel.ParentID.Value; } - // Check if the channel, or its category, or the user is ignored + // Check if the channel or its category is ignored if ( - guild.Channels.IgnoredChannels.Contains(channelId.Value.Value) - || categoryId != null && guild.Channels.IgnoredChannels.Contains(categoryId.Value.Value) + guild.Messages.IgnoredChannels.Contains(channelId.Value.Value) + || categoryId != null && guild.Messages.IgnoredChannels.Contains(categoryId.Value.Value) ) return null; + if (userId != null) { - if (guild.Channels.IgnoredUsers.Contains(userId.Value)) - return null; - // Check the channel-local and category-local ignored users var channelIgnoredUsers = - guild.Channels.IgnoredUsersPerChannel.GetValueOrDefault(channelId.Value.Value) + guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault(channelId.Value.Value) ?? []; var categoryIgnoredUsers = ( categoryId != null - ? guild.Channels.IgnoredUsersPerChannel.GetValueOrDefault( + ? guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault( categoryId.Value.Value ) : [] @@ -310,36 +373,24 @@ public class WebhookExecutorService( } // These three events can be redirected to other channels. Redirects can be on a channel or category level. - // Obviously, the events are only redirected if they're supposed to be logged in the first place. - if ( - logChannelType - is LogChannelType.MessageUpdate - or LogChannelType.MessageDelete - or LogChannelType.MessageDeleteBulk - ) - { - if (GetDefaultLogChannel(guild, logChannelType) == 0) - return null; + // The events are only redirected if they're supposed to be logged in the first place. + if (GetDefaultLogChannel(guild, logChannelType) == 0) + return null; - var categoryRedirect = - categoryId != null - ? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value) - : 0; + var categoryRedirect = + categoryId != null + ? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value) + : 0; - if ( - guild.Channels.Redirects.TryGetValue(channelId.Value.Value, out var channelRedirect) - ) - return channelRedirect; - return categoryRedirect != 0 - ? categoryRedirect - : GetDefaultLogChannel(guild, logChannelType); - } - - return GetDefaultLogChannel(guild, logChannelType); + if (guild.Channels.Redirects.TryGetValue(channelId.Value.Value, out var channelRedirect)) + return channelRedirect; + return categoryRedirect != 0 + ? categoryRedirect + : GetDefaultLogChannel(guild, logChannelType); } - public static ulong GetDefaultLogChannel(Guild guild, LogChannelType channelType) => - channelType switch + public static ulong GetDefaultLogChannel(Guild guild, LogChannelType logChannelType) => + logChannelType switch { LogChannelType.GuildUpdate => guild.Channels.GuildUpdate, LogChannelType.GuildEmojisUpdate => guild.Channels.GuildEmojisUpdate, @@ -364,7 +415,7 @@ public class WebhookExecutorService( LogChannelType.MessageUpdate => guild.Channels.MessageUpdate, LogChannelType.MessageDelete => guild.Channels.MessageDelete, LogChannelType.MessageDeleteBulk => guild.Channels.MessageDeleteBulk, - _ => throw new ArgumentOutOfRangeException(nameof(channelType)), + _ => throw new ArgumentOutOfRangeException(nameof(logChannelType)), }; } diff --git a/Catalogger.GoImporter/GuildImport.cs b/Catalogger.GoImporter/GuildImport.cs index a578430..2f034ce 100644 --- a/Catalogger.GoImporter/GuildImport.cs +++ b/Catalogger.GoImporter/GuildImport.cs @@ -62,10 +62,14 @@ public static class GuildImport GoGuild guild ) { - var channels = new Guild.ChannelConfig + var messages = new Guild.MessageConfig { IgnoredChannels = guild.IgnoredChannels.ToList(), IgnoredUsers = guild.IgnoredUsers.ToList(), + }; + + var channels = new Guild.ChannelConfig + { GuildUpdate = guild.Channels.TryParse("GUILD_UPDATE"), GuildEmojisUpdate = guild.Channels.TryParse("GUILD_EMOJIS_UPDATE"), GuildRoleCreate = guild.Channels.TryParse("GUILD_ROLE_CREATE"), @@ -97,13 +101,14 @@ public static class GuildImport await conn.ExecuteAsync( """ - insert into guilds (id, channels, banned_systems, key_roles) - values (@Id, @Channels::jsonb, @BannedSystems, @KeyRoles) + insert into guilds (id, channels, messages, banned_systems, key_roles) + values (@Id, @channels::jsonb, @messages::jsonb, @BannedSystems, @KeyRoles) """, new { guild.Id, - Channels = channels, + messages, + channels, guild.BannedSystems, guild.KeyRoles, },