Skip to content

Commit

Permalink
Implement a help command for text cmds & fix global cmd registration
Browse files Browse the repository at this point in the history
  • Loading branch information
FloatingMilkshake committed Dec 17, 2024
1 parent 1a03dee commit dee9be8
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 107 deletions.
355 changes: 355 additions & 0 deletions Commands/GlobalCmds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
using System.Reflection;

namespace Cliptok.Commands
{
public class GlobalCmds
{
// These commands will be registered outside of the home server and can be used anywhere, even in DMs.

// Most of this is taken from DSharpPlus.CommandsNext and adapted to fit here.
// https://github.com/DSharpPlus/DSharpPlus/blob/1c1aa15/DSharpPlus.CommandsNext/CommandsNextExtension.cs#L829
[Command("helptextcmd"), Description("Displays command help.")]
[TextAlias("help")]
[AllowedProcessors(typeof(TextCommandProcessor))]
public async Task Help(CommandContext ctx, [Description("Command to provide help for."), RemainingText] string command = "")
{
var commandSplit = command.Split(' ');

DiscordEmbedBuilder helpEmbed = new()
{
Title = "Help",
Color = new DiscordColor("#0080ff")
};

IEnumerable<Command> cmds = ctx.Extension.Commands.Values.Where(cmd =>
cmd.Attributes.Any(attr => attr is AllowedProcessorsAttribute apAttr
&& apAttr.Processors.Contains(typeof(TextCommandProcessor))));

if (commandSplit.Length != 0 && commandSplit[0] != "")
{
commandSplit[0] += "textcmd";

Command? cmd = null;
IEnumerable<Command>? searchIn = cmds;
foreach (string c in commandSplit)
{
if (searchIn is null)
{
cmd = null;
break;
}

StringComparison comparison = StringComparison.InvariantCultureIgnoreCase;
StringComparer comparer = StringComparer.InvariantCultureIgnoreCase;
cmd = searchIn.FirstOrDefault(xc => xc.Name.Equals(c, comparison) || ((xc.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases.Contains(c.Replace("textcmd", ""), comparer) ?? false));

if (cmd is null)
{
break;
}

IEnumerable<ContextCheckAttribute> failedChecks = await CheckPermissionsAsync(ctx, cmd);
if (failedChecks.Any())
{
return;
}

searchIn = cmd.Subcommands.Any() ? cmd.Subcommands : null;
}

if (cmd is null)
{
throw new CommandNotFoundException(string.Join(" ", commandSplit));
}

helpEmbed.Description = $"`{cmd.Name.Replace("textcmd", "")}`: {cmd.Description ?? "No description provided."}";


if (cmd.Subcommands.Count > 0 && cmd.Subcommands.Any(subCommand => subCommand.Attributes.Any(attr => attr is DefaultGroupCommandAttribute)))
{
helpEmbed.Description += "\n\nThis group can be executed as a standalone command.";
}

var aliases = cmd.Method?.GetCustomAttributes<TextAliasAttribute>().FirstOrDefault()?.Aliases ?? (cmd.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases ?? null;
if (aliases is not null && aliases.Length > 1)
{
var aliasStr = "";
foreach (var alias in aliases)
{
if (alias == cmd.Name.Replace("textcmd", ""))
continue;

aliasStr += $"`{alias}`, ";
}
aliasStr = aliasStr.TrimEnd(',', ' ');
helpEmbed.AddField("Aliases", aliasStr);
}

var arguments = cmd.Method?.GetParameters();
if (arguments is not null && arguments.Length > 0)
{
var argumentsStr = $"`{cmd.Name.Replace("textcmd", "")}";
foreach (var arg in arguments)
{
if (arg.ParameterType is CommandContext || arg.ParameterType.IsSubclassOf(typeof(CommandContext)))
continue;

bool isCatchAll = arg.GetCustomAttribute<RemainingTextAttribute>() != null;
argumentsStr += $"{(arg.IsOptional || isCatchAll ? " [" : " <")}{arg.Name}{(isCatchAll ? "..." : "")}{(arg.IsOptional || isCatchAll ? "]" : ">")}";
}

argumentsStr += "`\n";

foreach (var arg in arguments)
{
if (arg.ParameterType is CommandContext || arg.ParameterType.IsSubclassOf(typeof(CommandContext)))
continue;

argumentsStr += $"`{arg.Name} ({arg.ParameterType.Name})`: {arg.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "No description provided."}\n";
}

helpEmbed.AddField("Arguments", argumentsStr.Trim());
}
//helpBuilder.WithCommand(cmd);

if (cmd.Subcommands.Any())
{
IEnumerable<Command> commandsToSearch = cmd.Subcommands;
List<Command> eligibleCommands = [];
foreach (Command? candidateCommand in commandsToSearch)
{
var executionChecks = candidateCommand.Attributes.Where(x => x is ContextCheckAttribute) as List<ContextCheckAttribute>;

if (executionChecks == null || !executionChecks.Any())
{
eligibleCommands.Add(candidateCommand);
continue;
}

IEnumerable<ContextCheckAttribute> candidateFailedChecks = await CheckPermissionsAsync(ctx, candidateCommand);
if (!candidateFailedChecks.Any())
{
eligibleCommands.Add(candidateCommand);
}
}

if (eligibleCommands.Count != 0)
{
eligibleCommands = eligibleCommands.OrderBy(x => x.Name).ToList();
string cmdList = "";
foreach (var subCommand in eligibleCommands)
{
cmdList += $"`{subCommand.Name}`, ";
}
helpEmbed.AddField("Subcommands", cmdList.TrimEnd(',', ' '));
//helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
}
}
}
else
{
IEnumerable<Command> commandsToSearch = cmds;
List<Command> eligibleCommands = [];
foreach (Command? sc in commandsToSearch)
{
var executionChecks = sc.Attributes.Where(x => x is ContextCheckAttribute);

if (!executionChecks.Any())
{
eligibleCommands.Add(sc);
continue;
}

IEnumerable<ContextCheckAttribute> candidateFailedChecks = await CheckPermissionsAsync(ctx, sc);
if (!candidateFailedChecks.Any())
{
eligibleCommands.Add(sc);
}
}

if (eligibleCommands.Count != 0)
{
eligibleCommands = eligibleCommands.OrderBy(x => x.Name).ToList();
string cmdList = "";
foreach (var eligibleCommand in eligibleCommands)
{
cmdList += $"`{eligibleCommand.Name.Replace("textcmd", "")}`, ";
}
helpEmbed.AddField("Commands", cmdList.TrimEnd(',', ' '));
helpEmbed.Description = "Listing all top-level commands and groups. Specify a command to see more information.";
//helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
}
}

DiscordMessageBuilder builder = new DiscordMessageBuilder().AddEmbed(helpEmbed);

await ctx.RespondAsync(builder);
}

[Command("pingtextcmd")]
[TextAlias("ping")]
[Description("Pong? This command lets you know whether I'm working well.")]
[AllowedProcessors(typeof(TextCommandProcessor))]
public async Task Ping(TextCommandContext ctx)
{
ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString());
DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging...");
ulong ping = (return_message.Id - ctx.Message.Id) >> 22;
char[] choices = new char[] { 'a', 'e', 'o', 'u', 'i', 'y' };
char letter = choices[Program.rand.Next(0, choices.Length)];
await return_message.ModifyAsync($"P{letter}ng! 🏓\n" +
$"• It took me `{ping}ms` to reply to your message!\n" +
$"• Last Websocket Heartbeat took `{Math.Round(ctx.Client.GetConnectionLatency(0).TotalMilliseconds, 0)}ms`!");
}

[Command("userinfo")]
[TextAlias("user-info", "whois")]
[Description("Show info about a user.")]
[AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))]
public async Task UserInfoSlashCommand(CommandContext ctx, [Parameter("user"), Description("The user to retrieve information about.")] DiscordUser user = null, [Parameter("public"), Description("Whether to show the output publicly.")] bool publicMessage = false)
{
if (user is null)
user = ctx.User;

await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage);
}

[Command("remindmetextcmd")]
[Description("Set a reminder for yourself. Example: !reminder 1h do the thing")]
[TextAlias("remindme", "reminder", "rember", "wemember", "remember", "remind")]
[AllowedProcessors(typeof(TextCommandProcessor))]
[RequireHomeserverPerm(ServerPermLevel.Tier4, WorkOutside = true)]
public async Task RemindMe(
TextCommandContext ctx,
[Description("The amount of time to wait before reminding you. For example: 2s, 5m, 1h, 1d")] string timetoParse,
[RemainingText, Description("The text to send when the reminder triggers.")] string reminder
)
{
DateTime t = HumanDateParser.HumanDateParser.Parse(timetoParse);
if (t <= DateTime.Now)
{
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time can't be in the past!");
return;
}
#if !DEBUG
else if (t < (DateTime.Now + TimeSpan.FromSeconds(59)))
{
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time must be at least a minute in the future!");
return;
}
#endif
string guildId;

if (ctx.Channel.IsPrivate)
guildId = "@me";
else
guildId = ctx.Guild.Id.ToString();

var reminderObject = new Reminder()
{
UserID = ctx.User.Id,
ChannelID = ctx.Channel.Id,
MessageID = ctx.Message.Id,
MessageLink = $"https://discord.com/channels/{guildId}/{ctx.Channel.Id}/{ctx.Message.Id}",
ReminderText = reminder,
ReminderTime = t,
OriginalTime = DateTime.Now
};

await Program.db.ListRightPushAsync("reminders", JsonConvert.SerializeObject(reminderObject));
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} I'll try my best to remind you about that on <t:{TimeHelpers.ToUnixTimestamp(t)}:f> (<t:{TimeHelpers.ToUnixTimestamp(t)}:R>)"); // (In roughly **{TimeHelpers.TimeToPrettyFormat(t.Subtract(ctx.Message.Timestamp.DateTime), false)}**)");
}

public class Reminder
{
[JsonProperty("userID")]
public ulong UserID { get; set; }

[JsonProperty("channelID")]
public ulong ChannelID { get; set; }

[JsonProperty("messageID")]
public ulong MessageID { get; set; }

[JsonProperty("messageLink")]
public string MessageLink { get; set; }

[JsonProperty("reminderText")]
public string ReminderText { get; set; }

[JsonProperty("reminderTime")]
public DateTime ReminderTime { get; set; }

[JsonProperty("originalTime")]
public DateTime OriginalTime { get; set; }
}

// Runs command context checks manually. Returns a list of failed checks.
// Unfortunately DSharpPlus.Commands does not provide a way to execute a command's context checks manually,
// so this will have to do. This may not include all checks, but it includes everything I could think of. -Milkshake
private async Task<IEnumerable<ContextCheckAttribute>> CheckPermissionsAsync(CommandContext ctx, Command cmd)
{
var contextChecks = cmd.Attributes.Where(x => x is ContextCheckAttribute);
var failedChecks = new List<ContextCheckAttribute>();

foreach (var check in contextChecks)
{
if (check is HomeServerAttribute homeServerAttribute)
{
if (ctx.Channel.IsPrivate || ctx.Guild is null || ctx.Guild.Id != Program.cfgjson.ServerID)
{
failedChecks.Add(homeServerAttribute);
}
}

if (check is RequireHomeserverPermAttribute requireHomeserverPermAttribute)
{
if (ctx.Member is null && !requireHomeserverPermAttribute.WorkOutside)
{
failedChecks.Add(requireHomeserverPermAttribute);
}
else
{
if (!requireHomeserverPermAttribute.WorkOutside)
{
var level = await GetPermLevelAsync(ctx.Member);
if (level < requireHomeserverPermAttribute.TargetLvl)
{
failedChecks.Add(requireHomeserverPermAttribute);
}
}
}

}

if (check is RequirePermissionsAttribute requirePermissionsAttribute)
{
if (ctx.Member is null || ctx.Guild is null
|| !ctx.Channel.PermissionsFor(ctx.Member).HasAllPermissions(requirePermissionsAttribute.UserPermissions)
|| !ctx.Channel.PermissionsFor(ctx.Guild.CurrentMember).HasAllPermissions(requirePermissionsAttribute.BotPermissions))
{
failedChecks.Add(requirePermissionsAttribute);
}
}

if (check is IsBotOwnerAttribute isBotOwnerAttribute)
{
if (!Program.cfgjson.BotOwners.Contains(ctx.User.Id))
{
failedChecks.Add(isBotOwnerAttribute);
}
}

if (check is UserRolesPresentAttribute userRolesPresentAttribute)
{
if (Program.cfgjson.UserRoles is null)
{
failedChecks.Add(userRolesPresentAttribute);
}
}
}

return failedChecks;
}
}
}
Loading

0 comments on commit dee9be8

Please sign in to comment.