From 223f8081512b667b53561eba4bd46b9de8e2d401 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 18 Nov 2024 21:26:47 +0100 Subject: [PATCH] feat: ignore entity commands, actually ignore events when the entity is ignored --- .../Bot/Commands/IgnoreEntitiesCommands.cs | 304 ++++++++++++++++++ .../IgnoreMessageCommands.Channels.cs | 1 - Catalogger.Backend/Program.cs | 3 + .../Services/WebhookExecutorService.cs | 43 ++- 4 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs diff --git a/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs b/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs new file mode 100644 index 0000000..5ffc7a9 --- /dev/null +++ b/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs @@ -0,0 +1,304 @@ +// 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")] +[Description("Manage the ignored channels and roles in this server.")] +[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] +public class IgnoreEntitiesCommands : CommandGroup +{ + [Group("role")] + 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.IgnoredRoles.Contains(role.ID.Value)) + return await feedbackService.ReplyAsync( + "That role is already being ignored.", + isEphemeral: true + ); + + guildConfig.IgnoredRoles.Add(role.ID.Value); + await guildRepository.UpdateConfigAsync(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.IgnoredRoles.Contains(role.ID.Value)) + return await feedbackService.ReplyAsync( + "That role is already not ignored.", + isEphemeral: true + ); + + guildConfig.IgnoredRoles.Remove(role.ID.Value); + await guildRepository.UpdateConfigAsync(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.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(), + ] + ); + } + } + + [Group("channel")] + 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.")] + 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.IgnoredChannels.Contains(channel.ID.Value)) + return await feedbackService.ReplyAsync( + "That channel is already being ignored.", + isEphemeral: true + ); + + guildConfig.IgnoredChannels.Add(channel.ID.Value); + await guildRepository.UpdateConfigAsync(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.IgnoredChannels.Contains(channel.ID.Value)) + return await feedbackService.ReplyAsync( + "That channel is already not ignored.", + isEphemeral: true + ); + + guildConfig.IgnoredChannels.Remove(channel.ID.Value); + await guildRepository.UpdateConfigAsync(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 + .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 index a365c9f..69b225e 100644 --- a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs +++ b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs @@ -50,7 +50,6 @@ public partial class IgnoreMessageCommands : CommandGroup { [Command("add")] [Description("Add a channel to the list of ignored channels.")] - [SuppressInteractionResponse(true)] public async Task AddIgnoredChannelAsync( [ChannelTypes( ChannelType.GuildCategory, diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index f69c42e..94e785a 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -100,6 +100,9 @@ builder .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() // End command tree diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index 221b4a1..f82ea75 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -295,8 +295,8 @@ public class WebhookExecutorService( if (isMessageLog) return GetMessageLogChannel(guild, logChannelType, channelId, userId); - if (isChannelLog && guild.IgnoredChannels.Contains(channelId!.Value.Value)) - return null; + if (isChannelLog) + return GetChannelLogChannel(guild, logChannelType, channelId!.Value); if (isRoleLog && guild.IgnoredRoles.Contains(roleId!.Value.Value)) return null; @@ -309,6 +309,45 @@ public class WebhookExecutorService( return GetDefaultLogChannel(guild, logChannelType); } + private ulong? GetChannelLogChannel( + Guild guild, + LogChannelType logChannelType, + Snowflake channelId + ) + { + if (!channelCache.TryGet(channelId, out var channel)) + return GetDefaultLogChannel(guild, logChannelType); + + Snowflake? categoryId; + if ( + channel.Type + is ChannelType.AnnouncementThread + or ChannelType.PrivateThread + or ChannelType.PublicThread + ) + { + // parent_id should always have a value for threads + channelId = channel.ParentID.Value!.Value; + if (!channelCache.TryGet(channelId, out var parentChannel)) + return GetDefaultLogChannel(guild, logChannelType); + categoryId = parentChannel.ParentID.Value; + } + else + { + channelId = channel.ID; + categoryId = channel.ParentID.Value; + } + + // Check if the channel or its category is ignored + if ( + guild.IgnoredChannels.Contains(channelId.Value) + || (categoryId != null && guild.IgnoredChannels.Contains(categoryId.Value.Value)) + ) + return null; + + return GetDefaultLogChannel(guild, logChannelType); + } + private ulong? GetMessageLogChannel( Guild guild, LogChannelType logChannelType,