From 73ff3cc76c95c93b72ab673e459f3abdfab1f8f0 Mon Sep 17 00:00:00 2001 From: SylveonDeko <59923820+SylveonDeko@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:32:21 -0400 Subject: [PATCH] Better help command --- src/Mewdeko/Modules/Help/Help.cs | 129 +++++++++++------- src/Mewdeko/Modules/Help/HelpSlashCommand.cs | 135 ++++++++++++------- 2 files changed, 163 insertions(+), 101 deletions(-) diff --git a/src/Mewdeko/Modules/Help/Help.cs b/src/Mewdeko/Modules/Help/Help.cs index 063a0c02b..95f2c2681 100644 --- a/src/Mewdeko/Modules/Help/Help.cs +++ b/src/Mewdeko/Modules/Help/Help.cs @@ -112,7 +112,7 @@ await ctx.Channel.SendErrorAsync( /// The term to search for [Cmd] [Aliases] - public async Task SearchCommand(string commandname) + public async Task SearchCommand([Remainder] string commandname) { var commandInfos = cmds.Commands.Distinct() .Where(c => c.Name.Contains(commandname, StringComparison.InvariantCulture)); @@ -192,29 +192,15 @@ public async Task Commands([Remainder] string? module = null) } var prefix = await guildSettings.GetPrefix(ctx.Guild); - // Find commands for that module - // don't show commands which are blocked - // order by name - var commandInfos = cmds.Commands.Where(c => - c.Module.GetTopLevelModule().Name.ToUpperInvariant() - .StartsWith(module, StringComparison.InvariantCulture)) - .Where(c => !perms.BlockedCommands.Contains(c.Aliases[0].ToLowerInvariant())) - .OrderBy(c => c.Aliases[0]) - .Distinct(new CommandTextEqualityComparer()); - - // check preconditions for all commands, but only if it's not 'all' - // because all will show all commands anyway, no need to check - var succ = new HashSet((await Task.WhenAll(commandInfos.Select(async x => - { - var pre = await x.CheckPreconditionsAsync(Context, services).ConfigureAwait(false); - return (Cmd: x, Succ: pre.IsSuccess); - })).ConfigureAwait(false)) - .Where(x => x.Succ) - .Select(x => x.Cmd)); - var cmdsWithGroup = commandInfos - .GroupBy(c => c.Module.Name.Replace("Commands", "", StringComparison.InvariantCulture)) - .OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count()); + // Pre-filter commands and create a lookup for blocked commands + var blockedCommandsSet = new HashSet(perms.BlockedCommands.Select(c => c.ToLowerInvariant())); + var commandInfos = cmds.Commands + .Where(c => c.Module.GetTopLevelModule().Name.ToUpperInvariant() + .StartsWith(module, StringComparison.InvariantCulture) && + !blockedCommandsSet.Contains(c.Aliases[0].ToLowerInvariant())) + .Distinct(new CommandTextEqualityComparer()) + .ToList(); if (!commandInfos.Any()) { @@ -222,45 +208,90 @@ public async Task Commands([Remainder] string? module = null) return; } - var i = 0; - var groups = cmdsWithGroup.GroupBy(_ => i++ / 48).ToArray(); + // Check preconditions + var preconditionTasks = commandInfos.Select(async x => + { + var pre = await x.CheckPreconditionsAsync(Context, services).ConfigureAwait(false); + return (Cmd: x, Succ: pre.IsSuccess); + }); + var preconditionResults = await Task.WhenAll(preconditionTasks).ConfigureAwait(false); + var succ = new HashSet(preconditionResults.Where(x => x.Succ).Select(x => x.Cmd)); + + // Group and sort commands, ensuring no duplicates + var seenCommands = new HashSet(); + var cmdsWithGroup = commandInfos + .GroupBy(c => c.Module.Name.Replace("Commands", "", StringComparison.InvariantCulture)) + .Select(g => new + { + ModuleName = g.Key, + Commands = g.Where(c => seenCommands.Add(c.Aliases[0].ToLowerInvariant())) + .OrderBy(c => c.Aliases[0]) + .ToList() + }) + .Where(g => g.Commands.Any()) + .OrderBy(g => g.ModuleName) + .ToList(); + + var pageSize = 24; + var totalCommands = cmdsWithGroup.Sum(g => g.Commands.Count); + var totalPages = (int)Math.Ceiling(totalCommands / (double)pageSize); + var paginator = new LazyPaginatorBuilder() .AddUser(ctx.User) .WithPageFactory(PageFactory) .WithFooter(PaginatorFooter.PageNumber | PaginatorFooter.Users) - .WithMaxPageIndex(groups.Select(x => x.Count()).FirstOrDefault() - 1) + .WithMaxPageIndex(totalPages - 1) .WithDefaultEmotes() .WithActionOnCancellation(ActionOnStop.DeleteMessage) .Build(); - await serv.SendPaginatorAsync(paginator, Context.Channel, - TimeSpan.FromMinutes(60)).ConfigureAwait(false); + await serv.SendPaginatorAsync(paginator, Context.Channel, TimeSpan.FromMinutes(60)).ConfigureAwait(false); - async Task PageFactory(int page) + Task PageFactory(int page) { - await Task.CompletedTask.ConfigureAwait(false); - var transformed = groups.Select(x => x.ElementAt(page) - .Where(commandInfo => !commandInfo.Attributes.Any(attribute => attribute is HelpDisabled)).Select( - commandInfo => - $"{(succ.Contains(commandInfo) ? commandInfo.Preconditions.Any(preconditionAttribute => preconditionAttribute is RequireDragonAttribute) ? "šŸ‰" : "āœ…" : "āŒ")}{prefix + commandInfo.Aliases[0]}{(commandInfo.Aliases.Skip(1).FirstOrDefault() is not null ? $"/{prefix}{commandInfo.Aliases[1]}" : "")}")) - .FirstOrDefault(); - var last = groups.Select(x => x.Count()).FirstOrDefault(); - for (i = 0; i < last; i++) + var pageBuilder = new PageBuilder().WithOkColor(); + var commandsOnPage = new List(); + var currentModule = ""; + var commandCount = 0; + + foreach (var group in cmdsWithGroup) { - if (i != last - 1 || (i + 1) % 1 == 0) continue; - var grp = 0; - var count = transformed.Count(); - transformed = transformed - .GroupBy(_ => grp++ % count / 2) - .Select(x => x.Count() == 1 ? $"{x.First()}" : string.Concat(x)); + foreach (var cmd in group.Commands) + { + if (commandCount >= page * pageSize && commandCount < (page + 1) * pageSize) + { + if (currentModule != group.ModuleName) + { + if (commandsOnPage.Any()) + pageBuilder.AddField(currentModule, + $"```css\n{string.Join("\n", commandsOnPage)}\n```"); + commandsOnPage.Clear(); + currentModule = group.ModuleName; + } + + var cmdString = + $"{(succ.Contains(cmd) ? cmd.Preconditions.Any(p => p is RequireDragonAttribute) ? "šŸ‰" : "āœ…" : "āŒ")}" + + $"{prefix}{cmd.Aliases[0]}" + + $"{(cmd.Aliases.Skip(1).FirstOrDefault() is not null ? $"/{prefix}{cmd.Aliases[1]}" : "")}"; + commandsOnPage.Add(cmdString); + } + + commandCount++; + if (commandCount >= (page + 1) * pageSize) break; + } + + if (commandCount >= (page + 1) * pageSize) break; } - return new PageBuilder() - .AddField(groups.Select(x => x.ElementAt(page).Key).FirstOrDefault(), - $"```css\n{string.Join("\n", transformed)}\n```") - .WithDescription( - $"āœ…: You can use this command.\nāŒ: You cannot use this command.\n{config.Data.LoadingEmote}: If you need any help don't hesitate to join [The Support Server](https://discord.gg/mewdeko)\nDo `{prefix}h commandname` to see info on that command") - .WithOkColor(); + if (commandsOnPage.Any()) + pageBuilder.AddField(currentModule, $"```css\n{string.Join("\n", commandsOnPage)}\n```"); + + pageBuilder.WithDescription( + $"āœ…: You can use this command.\nāŒ: You cannot use this command.\n" + + $"{config.Data.LoadingEmote}: If you need any help don't hesitate to join [The Support Server](https://discord.gg/mewdeko)\n" + + $"Do `{prefix}h commandname` to see info on that command"); + + return Task.FromResult(pageBuilder); } } diff --git a/src/Mewdeko/Modules/Help/HelpSlashCommand.cs b/src/Mewdeko/Modules/Help/HelpSlashCommand.cs index 16b57e5b2..87987972b 100644 --- a/src/Mewdeko/Modules/Help/HelpSlashCommand.cs +++ b/src/Mewdeko/Modules/Help/HelpSlashCommand.cs @@ -2,6 +2,7 @@ using Discord.Interactions; using Fergun.Interactive; using Fergun.Interactive.Pagination; +using LinqToDB.Tools; using Mewdeko.Common.Attributes.InteractionCommands; using Mewdeko.Common.Attributes.TextCommands; using Mewdeko.Common.Autocompleters; @@ -10,6 +11,7 @@ using Mewdeko.Modules.Help.Services; using Mewdeko.Modules.Permissions.Services; using Mewdeko.Services.Settings; +using RequireDragonAttribute = Mewdeko.Common.Attributes.InteractionCommands.RequireDragonAttribute; namespace Mewdeko.Modules.Help; @@ -31,7 +33,7 @@ public class HelpSlashCommand( CommandService cmds, CommandHandler ch, GuildSettingsService guildSettings, - BotConfigService config) + BotConfigService config, GlobalPermissionService perms) : MewdekoSlashModuleBase { private static readonly ConcurrentDictionary HelpMessages = new(); @@ -84,29 +86,15 @@ public async Task HelpSlash(string unused, string[] selected) } var prefix = await guildSettings.GetPrefix(ctx.Guild); - // Find commands for that module - // don't show commands which are blocked - // order by name - var commandInfos = cmds.Commands.Where(c => - c.Module.GetTopLevelModule().Name.ToUpperInvariant() - .StartsWith(module, StringComparison.InvariantCulture) && - !permissionService.BlockedCommands.Contains(c.Aliases[0].ToLowerInvariant())) - .OrderBy(c => c.Aliases[0]) - .Distinct(new CommandTextEqualityComparer()); - // check preconditions for all commands, but only if it's not 'all' - // because all will show all commands anyway, no need to check - var succ = new HashSet((await Task.WhenAll(commandInfos.Select(async x => - { - var pre = await x.CheckPreconditionsAsync(new CommandContext(ctx.Client, currentmsg), serviceProvider) - .ConfigureAwait(false); - return (Cmd: x, Succ: pre.IsSuccess); - })).ConfigureAwait(false)) - .Where(x => x.Succ) - .Select(x => x.Cmd)); - var cmdsWithGroup = commandInfos - .GroupBy(c => c.Module.Name.Replace("Commands", "", StringComparison.InvariantCulture)) - .OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count()); + // Pre-filter commands and create a lookup for blocked commands + var blockedCommandsSet = new HashSet(perms.BlockedCommands.Select(c => c.ToLowerInvariant())); + var commandInfos = cmds.Commands + .Where(c => c.Module.GetTopLevelModule().Name.ToUpperInvariant() + .StartsWith(module, StringComparison.InvariantCulture) && + !blockedCommandsSet.Contains(c.Aliases[0].ToLowerInvariant())) + .Distinct(new CommandTextEqualityComparer()) + .ToList(); if (!commandInfos.Any()) { @@ -114,47 +102,90 @@ public async Task HelpSlash(string unused, string[] selected) return; } - var i = 0; - var groups = cmdsWithGroup.GroupBy(_ => i++ / 48).ToArray(); + // Check preconditions + var preconditionTasks = commandInfos.Select(async x => + { + var pre = await x.CheckPreconditionsAsync(new CommandContext(ctx.Client, currentmsg), serviceProvider); + return (Cmd: x, Succ: pre.IsSuccess); + }); + var preconditionResults = await Task.WhenAll(preconditionTasks).ConfigureAwait(false); + var succ = new HashSet(preconditionResults.Where(x => x.Succ).Select(x => x.Cmd)); + + // Group and sort commands, ensuring no duplicates + var seenCommands = new HashSet(); + var cmdsWithGroup = commandInfos + .GroupBy(c => c.Module.Name.Replace("Commands", "", StringComparison.InvariantCulture)) + .Select(g => new + { + ModuleName = g.Key, + Commands = g.Where(c => seenCommands.Add(c.Aliases[0].ToLowerInvariant())) + .OrderBy(c => c.Aliases[0]) + .ToList() + }) + .Where(g => g.Commands.Any()) + .OrderBy(g => g.ModuleName) + .ToList(); + + var pageSize = 24; + var totalCommands = cmdsWithGroup.Sum(g => g.Commands.Count); + var totalPages = (int)Math.Ceiling(totalCommands / (double)pageSize); + var paginator = new LazyPaginatorBuilder() .AddUser(ctx.User) .WithPageFactory(PageFactory) .WithFooter(PaginatorFooter.PageNumber | PaginatorFooter.Users) - .WithMaxPageIndex(groups.Select(x => x.Count()).FirstOrDefault() - 1) + .WithMaxPageIndex(totalPages - 1) .WithDefaultEmotes() + .WithActionOnCancellation(ActionOnStop.DeleteMessage) .Build(); - var msg = await interactivity.SendPaginatorAsync(paginator, ctx.Interaction as SocketInteraction, - TimeSpan.FromMinutes(60)).ConfigureAwait(false); - - HelpMessages.TryAdd(ctx.Channel.Id, msg.Message.Id); - + await interactivity.SendPaginatorAsync(paginator, ctx.Interaction, TimeSpan.FromMinutes(60)).ConfigureAwait(false); - async Task PageFactory(int page) + Task PageFactory(int page) { - await Task.CompletedTask.ConfigureAwait(false); - var transformed = groups.Select(x => x.ElementAt(page) - .Where(commandInfo => !commandInfo.Attributes.Any(attribute => attribute is HelpDisabled)).Select( - commandInfo => - $"{(succ.Contains(commandInfo) ? "āœ…" : "āŒ")}{prefix + commandInfo.Aliases[0]}{(commandInfo.Aliases.Skip(1).FirstOrDefault() is not null ? $"/{prefix}{commandInfo.Aliases[1]}" : "")}")) - .FirstOrDefault(); - var last = groups.Select(x => x.Count()).FirstOrDefault(); - for (i = 0; i < last; i++) + var pageBuilder = new PageBuilder().WithOkColor(); + var commandsOnPage = new List(); + var currentModule = ""; + var commandCount = 0; + + foreach (var group in cmdsWithGroup) { - if (i != last - 1 || (i + 1) % 1 == 0) continue; - var grp = 0; - var count = transformed.Count(); - transformed = transformed - .GroupBy(_ => grp++ % count / 2) - .Select(x => x.Count() == 1 ? $"{x.First()}" : string.Concat(x)); + foreach (var cmd in group.Commands) + { + if (commandCount >= page * pageSize && commandCount < (page + 1) * pageSize) + { + if (currentModule != group.ModuleName) + { + if (commandsOnPage.Any()) + pageBuilder.AddField(currentModule, + $"```css\n{string.Join("\n", commandsOnPage)}\n```"); + commandsOnPage.Clear(); + currentModule = group.ModuleName; + } + + var cmdString = + $"{(succ.Contains(cmd) ? cmd.Preconditions.Any(p => p is RequireDragonAttribute) ? "šŸ‰" : "āœ…" : "āŒ")}" + + $"{prefix}{cmd.Aliases[0]}" + + $"{(cmd.Aliases.Skip(1).FirstOrDefault() is not null ? $"/{prefix}{cmd.Aliases[1]}" : "")}"; + commandsOnPage.Add(cmdString); + } + + commandCount++; + if (commandCount >= (page + 1) * pageSize) break; + } + + if (commandCount >= (page + 1) * pageSize) break; } - return new PageBuilder() - .AddField(groups.Select(x => x.ElementAt(page).Key).FirstOrDefault(), - $"```css\n{string.Join("\n", transformed)}\n```") - .WithDescription( - $"āœ…: You can use this command.\nāŒ: You cannot use this command.\n{config.Data.LoadingEmote}: If you need any help don't hesitate to join [The Support Server](https://discord.gg/mewdeko)\nDo `{prefix}h commandname` to see info on that command") - .WithOkColor(); + if (commandsOnPage.Any()) + pageBuilder.AddField(currentModule, $"```css\n{string.Join("\n", commandsOnPage)}\n```"); + + pageBuilder.WithDescription( + $"āœ…: You can use this command.\nāŒ: You cannot use this command.\n" + + $"{config.Data.LoadingEmote}: If you need any help don't hesitate to join [The Support Server](https://discord.gg/mewdeko)\n" + + $"Do `{prefix}h commandname` to see info on that command"); + + return Task.FromResult(pageBuilder); } }