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);