-
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement a help command for text cmds & fix global cmd registration
- Loading branch information
1 parent
1a03dee
commit dee9be8
Showing
6 changed files
with
362 additions
and
107 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.