diff --git a/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs b/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs index d59174c..a60ceb3 100644 --- a/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs +++ b/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs @@ -166,7 +166,7 @@ public class IgnoreChannelCommands( var value = string.Join("\n", visibleCategories.Select(c => $"<#{c.Id}>")); if (nonVisibleCategories != 0) value += - $"\n\n{nonVisibleCategories} channel(s) were ignored as you do not have access to them."; + $"\n\n{nonVisibleCategories} channel(s) are not shown as you do not have access to them."; embed.AddField("Categories", value); } @@ -183,7 +183,7 @@ public class IgnoreChannelCommands( var value = string.Join("\n", visibleBase.Select(c => $"<#{c.Id}>")); if (nonVisibleBase != 0) value += - $"\n\n{nonVisibleBase} channel(s) were ignored as you do not have access to them."; + $"\n\n{nonVisibleBase} channel(s) are not shown as you do not have access to them."; embed.AddField("Channels", value); } diff --git a/Catalogger.Backend/Bot/Commands/InviteCommands.cs b/Catalogger.Backend/Bot/Commands/InviteCommands.cs index 4afcb44..bace978 100644 --- a/Catalogger.Backend/Bot/Commands/InviteCommands.cs +++ b/Catalogger.Backend/Bot/Commands/InviteCommands.cs @@ -90,6 +90,12 @@ public class InviteCommands( )) .ToList(); + if (fields.Count == 0) + return await feedbackService.ReplyAsync( + "No invites found for this server.", + isEphemeral: true + ); + return await feedbackService.SendContextualPaginatedMessageAsync( userId, DiscordUtils.PaginateFields( diff --git a/Catalogger.Backend/Bot/Commands/MetaCommands.cs b/Catalogger.Backend/Bot/Commands/MetaCommands.cs index aa8b59d..1319a98 100644 --- a/Catalogger.Backend/Bot/Commands/MetaCommands.cs +++ b/Catalogger.Backend/Bot/Commands/MetaCommands.cs @@ -46,6 +46,7 @@ public class MetaCommands( IFeedbackService feedbackService, ContextInjectionService contextInjection, GuildCache guildCache, + RoleCache roleCache, ChannelCache channelCache, EmojiCache emojiCache, IDiscordRestChannelAPI channelApi @@ -109,7 +110,8 @@ public class MetaCommands( embed.AddField( "Numbers", $"{CataloggerMetrics.MessagesStored.Value:N0} messages " - + $"from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels, {emojiCache.Size:N0}", + + $"from {guildCache.Size:N0} servers\n" + + $"Cached {channelCache.Size:N0} channels, {roleCache.Size:N0} roles, {emojiCache.Size:N0} emojis", false ); diff --git a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs new file mode 100644 index 0000000..d5418d0 --- /dev/null +++ b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs @@ -0,0 +1,196 @@ +// 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; +using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Extensions; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Commands.Services; +using Remora.Discord.Pagination.Extensions; +using IResult = Remora.Results.IResult; + +namespace Catalogger.Backend.Bot.Commands; + +[Group("redirects")] +[Description("Commands for configuring log redirects.")] +[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] +public class RedirectCommands( + DatabaseContext db, + GuildCache guildCache, + ChannelCache channelCache, + ContextInjectionService contextInjectionService, + FeedbackService feedbackService +) : CommandGroup +{ + [Command("add")] + [Description("Redirect message logs from a channel or category to another channel.")] + public async Task AddRedirectAsync( + [ChannelTypes( + ChannelType.GuildCategory, + ChannelType.GuildText, + ChannelType.GuildAnnouncement, + ChannelType.GuildForum, + ChannelType.GuildMedia, + ChannelType.GuildVoice + )] + [Description("The channel to redirect logs from")] + IChannel source, + [ChannelTypes(ChannelType.GuildText)] + [Description("The channel to redirect logs to")] + IChannel target + ) + { + var (_, guildId) = contextInjectionService.GetUserAndGuild(); + var guildConfig = await db.GetGuildAsync(guildId); + guildConfig.Channels.Redirects[source.ID.Value] = target.ID.Value; + db.Update(guildConfig); + await db.SaveChangesAsync(); + + var output = + $"Success! Edited and deleted messages from {FormatChannel(source)} will now be redirected to <#{target.ID}>."; + + // Notify the user if the channel they started redirecting from was already covered by a category-level redirect. + if ( + source.ParentID.IsDefined(out var parentId) + && guildConfig.Channels.Redirects.TryGetValue( + parentId.Value.Value, + out var parentRedirect + ) + && parentRedirect != 0 + ) + { + output += + $"\n**Note:** channels from the category {FormatChannel(source)} is in were already being redirected to <#{parentRedirect}>."; + } + + return await feedbackService.ReplyAsync(output); + } + + [Command("remove")] + [Description("Stop redirecting message logs from a channel.")] + public async Task RemoveRedirectAsync( + [ChannelTypes( + ChannelType.GuildCategory, + ChannelType.GuildText, + ChannelType.GuildAnnouncement, + ChannelType.GuildForum, + ChannelType.GuildMedia, + ChannelType.GuildVoice + )] + IChannel source + ) + { + var (_, guildId) = contextInjectionService.GetUserAndGuild(); + var guildConfig = await db.GetGuildAsync(guildId); + + var wasSet = guildConfig.Channels.Redirects.Remove(source.ID.Value); + var output = wasSet + ? $"Removed the redirect for {FormatChannel(source)}! Message logs from" + + $"{(source.Type == ChannelType.GuildCategory ? "that category's channels" : "that channel")}" + + "will now be logged to the default channel, if any." + : $"Message logs from {FormatChannel(source)} were already not being redirected to another channel."; + + // Warn the user if this is a non-category channel and *all* of this category's channels are being redirected. + if ( + source.ParentID.IsDefined(out var parentId) + && guildConfig.Channels.Redirects.TryGetValue( + parentId.Value.Value, + out var parentRedirect + ) + && parentRedirect != 0 + ) + { + var parentChannelName = $"<#{parentId}>"; + if (channelCache.TryGet(parentId.Value, out var parentChannel)) + parentChannelName = parentChannel.Name.Value!; + + if (wasSet) + output += + $"\nHowever, all channels in the {parentChannelName} category are being redirected to <#{parentRedirect}>, " + + $"so removing the redirect for {FormatChannel(source)} will not take effect until that is removed as well."; + else + output += + $"\nHowever, all channels in the {parentChannelName} category are being redirected to <#{parentRedirect}>."; + } + + db.Update(guildConfig); + await db.SaveChangesAsync(); + + return await feedbackService.ReplyAsync(output); + } + + [Command("list")] + [Description("List currently redirected channels.")] + public async Task ListRedirectsAsync() + { + var (userId, guildId) = contextInjectionService.GetUserAndGuild(); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild was not cached"); + var guildChannels = channelCache.GuildChannels(guildId).ToList(); + var guildConfig = await db.GetGuildAsync(guildId); + + var fields = new List(); + + foreach (var (source, target) in guildConfig.Channels.Redirects) + { + fields.Add( + new EmbedField( + Name: FormatChannelHeader( + source, + guildChannels.FirstOrDefault(c => c.ID.Value == source) + ), + Value: FormatChannelText( + target, + guildChannels.FirstOrDefault(c => c.ID.Value == target) + ), + IsInline: false + ) + ); + } + + if (fields.Count == 0) + return await feedbackService.ReplyAsync( + "No channels are being redirected right now.", + isEphemeral: true + ); + + return await feedbackService.SendContextualPaginatedMessageAsync( + userId, + DiscordUtils.PaginateFields(fields, $"Channel redirects for {guild.Name}") + ); + } + + private static string FormatChannel(IChannel channel) => + channel.Type == ChannelType.GuildCategory ? channel.Name.Value! : $"<#{channel.ID}>"; + + private static string FormatChannelHeader(ulong id, IChannel? channel) => + channel != null + ? channel.Type == ChannelType.GuildCategory + ? $"Category {channel.Name}" + : $"#{channel.Name}" + : $"*unknown channel {id}*"; + + private static string FormatChannelText(ulong id, IChannel? channel) => + channel != null + ? $"#{channel.Name} (<#{channel.ID}>)" + : $"*unknown channel {id}* (<#{id}>)"; +} diff --git a/Catalogger.Backend/Extensions/DiscordExtensions.cs b/Catalogger.Backend/Extensions/DiscordExtensions.cs index 02dc1d0..6c2f870 100644 --- a/Catalogger.Backend/Extensions/DiscordExtensions.cs +++ b/Catalogger.Backend/Extensions/DiscordExtensions.cs @@ -137,7 +137,7 @@ public static class DiscordExtensions permissionSet.GetPermissions().Select(p => p.Humanize(LetterCasing.Title)) ); - public static (Snowflake, Snowflake) GetUserAndGuild( + public static (Snowflake UserId, Snowflake GuildId) GetUserAndGuild( this ContextInjectionService contextInjectionService ) { diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index bdd712a..988c06c 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -88,6 +88,7 @@ builder .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() + .WithCommandGroup() // End command tree .Finish() .AddPagination() diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index c83350a..8cd2679 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -321,9 +321,9 @@ public class WebhookExecutorService( guild.Channels.Redirects.TryGetValue(channelId.Value.Value, out var channelRedirect) ) return channelRedirect; - if (categoryRedirect != 0) - return categoryRedirect; - return GetDefaultLogChannel(guild, logChannelType); + return categoryRedirect != 0 + ? categoryRedirect + : GetDefaultLogChannel(guild, logChannelType); } return GetDefaultLogChannel(guild, logChannelType);