Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 75 additions & 27 deletions Modules/HelpModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Morpheus.Extensions;
using Morpheus.Handlers;
using Morpheus.Utilities;
using System.Globalization;
using System.Reflection;

namespace Morpheus.Modules;
Expand Down Expand Up @@ -44,7 +45,7 @@ private async Task HandleHelpSelectorInteraction(SocketInteraction interaction)
Guild? guild = await dbContext.Guilds.FirstOrDefaultAsync(g => g.DiscordId == interaction.GuildId);
string commandPrefix = guild?.Prefix ?? Env.Variables["BOT_DEFAULT_COMMAND_PREFIX"];

string selectedModule = messageComponent.Data.Values.First();
string? selectedModule = messageComponent.Data.Values.FirstOrDefault();
Embed embed = CreateModuleHelpEmbed(selectedModule, commandPrefix);

// Fetch the original message using message ID
Expand All @@ -57,13 +58,57 @@ private async Task HandleHelpSelectorInteraction(SocketInteraction interaction)
}
}

private Embed CreateModuleHelpEmbed(string moduleName, string commandPrefix)
internal static bool TryParseHelpModuleName(string? moduleName, out int page, out string moduleKey)
{
page = 0;
moduleKey = string.Empty;

if (string.IsNullOrEmpty(moduleName))
return false;

int separatorIndex = moduleName.IndexOf('_');
if (separatorIndex <= 0 || separatorIndex == moduleName.Length - 1)
return false;

if (!int.TryParse(moduleName[..separatorIndex], NumberStyles.None, CultureInfo.InvariantCulture, out page) || page < 1)
return false;

moduleKey = moduleName[(separatorIndex + 1)..];
return moduleKey.IndexOf('_') < 0;
}

internal static bool TryGetHelpPageBounds(int page, int visibleCommandCount, out int startIndex, out int endIndex)
{
startIndex = 0;
endIndex = 0;

if (page < 1 || visibleCommandCount < 1)
return false;

long start = ((long)page - 1) * HelpPageSize;
if (start >= visibleCommandCount)
return false;

startIndex = (int)start;
endIndex = (int)Math.Min(start + HelpPageSize, visibleCommandCount);
return true;
}

private static Embed CreateInvalidHelpEmbed() => new EmbedBuilder()
{
Color = Colors.Blue,
Title = "Unable to load help",
Description = "The selected help page is invalid. Please run the help command again."
}.Build();

private Embed CreateModuleHelpEmbed(string? moduleName, string commandPrefix)
{
if (!TryParseHelpModuleName(moduleName, out int page, out string moduleKey))
return CreateInvalidHelpEmbed();

if (helpModules.TryGetValue(commandPrefix + moduleName, out Embed? embed))
return embed;

int page = int.Parse(moduleName.Split("_")[0]);
string moduleKey = moduleName.Split("_")[1];
string name = moduleKey.Replace("Module", "");

EmbedBuilder builder = new()
Expand All @@ -77,34 +122,37 @@ private Embed CreateModuleHelpEmbed(string moduleName, string commandPrefix)
string.Equals(m.Name, moduleKey, StringComparison.OrdinalIgnoreCase)
|| string.Equals(m.Name, moduleKey + "Module", StringComparison.OrdinalIgnoreCase));

if (module != null)
{
var visibleCommands = module.Commands.Where(c => !c.Attributes.OfType<HiddenAttribute>().Any()).ToList();
for (int i = (page - 1) * HelpPageSize; i < page * HelpPageSize && i < visibleCommands.Count; i++)
{
CommandInfo cmd = visibleCommands[i];
if (module == null)
return CreateInvalidHelpEmbed();

string aliases = cmd.Aliases.Count > 1
? $"Aliases: {string.Join(", ", cmd.Aliases.Skip(1).Select(a => commandPrefix + a))}"
: "No aliases available.";
var visibleCommands = module.Commands.Where(c => !c.Attributes.OfType<HiddenAttribute>().Any()).ToList();
if (!TryGetHelpPageBounds(page, visibleCommands.Count, out int startIndex, out int endIndex))
return CreateInvalidHelpEmbed();

string commandDescription = cmd.Summary ?? "No description available.";
if (commandDescription.Length > CommandDescriptionMaxLength)
{
commandDescription = commandDescription.Substring(0, CommandDescriptionMaxLength).TrimEnd() + "…";
}
for (int i = startIndex; i < endIndex; i++)
{
CommandInfo cmd = visibleCommands[i];

string commandUsage = cmd.Parameters.Count > 0 && cmd.Parameters.Any(p => p.Name != "_")
? $"Usage: `{commandPrefix}{cmd.Aliases[0]} {string.Join(" ", cmd.Parameters.Select(p => $"[{p.Name}{(p.IsOptional ? "?" : "")}{(!string.IsNullOrEmpty(p.DefaultValue?.ToString()) ? " = " + p.DefaultValue.ToString() : "")}]"))}`"
: $"Usage: `{commandPrefix}{cmd.Aliases[0]}`";
string aliases = cmd.Aliases.Count > 1
? $"Aliases: {string.Join(", ", cmd.Aliases.Skip(1).Select(a => commandPrefix + a))}"
: "No aliases available.";

builder.AddField(x =>
{
x.Name = cmd.Name;
x.Value = $"{commandDescription}\n{commandUsage}\n{aliases}";
x.IsInline = false;
});
string commandDescription = cmd.Summary ?? "No description available.";
if (commandDescription.Length > CommandDescriptionMaxLength)
{
commandDescription = commandDescription.Substring(0, CommandDescriptionMaxLength).TrimEnd() + "…";
}

string commandUsage = cmd.Parameters.Count > 0 && cmd.Parameters.Any(p => p.Name != "_")
? $"Usage: `{commandPrefix}{cmd.Aliases[0]} {string.Join(" ", cmd.Parameters.Select(p => $"[{p.Name}{(p.IsOptional ? "?" : "")}{(!string.IsNullOrEmpty(p.DefaultValue?.ToString()) ? " = " + p.DefaultValue.ToString() : "")}]"))}`"
: $"Usage: `{commandPrefix}{cmd.Aliases[0]}`";

builder.AddField(x =>
{
x.Name = cmd.Name;
x.Value = $"{commandDescription}\n{commandUsage}\n{aliases}";
x.IsInline = false;
});
}

embed = builder.Build();
Expand Down
46 changes: 46 additions & 0 deletions Morpheus.Tests/HelpCommandRegistrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,52 @@ namespace Morpheus.Tests;

public class HelpCommandRegistrationTests
{
[Theory]
[InlineData("2_StocksModule", 2, "StocksModule")]
[InlineData("10_Misc", 10, "Misc")]
public void TryParseHelpModuleName_ParsesPageAndModule(string input, int expectedPage, string expectedModule)
{
Assert.True(HelpModule.TryParseHelpModuleName(input, out int page, out string module));
Assert.Equal(expectedPage, page);
Assert.Equal(expectedModule, module);
}

[Theory]
[InlineData("")]
[InlineData("invalid")]
[InlineData("0_StocksModule")]
[InlineData("-1_StocksModule")]
[InlineData("2_")]
public void TryParseHelpModuleName_RejectsMalformedSelections(string input)
{
Assert.False(HelpModule.TryParseHelpModuleName(input, out _, out _));
}

[Fact]
public void TryParseHelpModuleName_RejectsAbsentSelection()
{
Assert.False(HelpModule.TryParseHelpModuleName(null, out _, out _));
}

[Theory]
[InlineData(1, 11, true, 0, 10)]
[InlineData(2, 11, true, 10, 11)]
[InlineData(3, 11, false, 0, 0)]
[InlineData(int.MaxValue, 11, false, 0, 0)]
public void TryGetHelpPageBounds_ValidatesActualPageRange(
int page,
int visibleCommandCount,
bool expectedResult,
int expectedStart,
int expectedEnd)
{
bool result = HelpModule.TryGetHelpPageBounds(page, visibleCommandCount, out int start, out int end);

Assert.Equal(expectedResult, result);
Assert.Equal(expectedStart, start);
Assert.Equal(expectedEnd, end);
}

[Fact]
public void HelpCommand_AcceptsMultiWordCommandNames()
{
Expand Down