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,