Skip to content

Commit dee9be8

Browse files
Implement a help command for text cmds & fix global cmd registration
1 parent 1a03dee commit dee9be8

File tree

6 files changed

+362
-107
lines changed

6 files changed

+362
-107
lines changed

Commands/GlobalCmds.cs

+355
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
using System.Reflection;
2+
3+
namespace Cliptok.Commands
4+
{
5+
public class GlobalCmds
6+
{
7+
// These commands will be registered outside of the home server and can be used anywhere, even in DMs.
8+
9+
// Most of this is taken from DSharpPlus.CommandsNext and adapted to fit here.
10+
// https://github.com/DSharpPlus/DSharpPlus/blob/1c1aa15/DSharpPlus.CommandsNext/CommandsNextExtension.cs#L829
11+
[Command("helptextcmd"), Description("Displays command help.")]
12+
[TextAlias("help")]
13+
[AllowedProcessors(typeof(TextCommandProcessor))]
14+
public async Task Help(CommandContext ctx, [Description("Command to provide help for."), RemainingText] string command = "")
15+
{
16+
var commandSplit = command.Split(' ');
17+
18+
DiscordEmbedBuilder helpEmbed = new()
19+
{
20+
Title = "Help",
21+
Color = new DiscordColor("#0080ff")
22+
};
23+
24+
IEnumerable<Command> cmds = ctx.Extension.Commands.Values.Where(cmd =>
25+
cmd.Attributes.Any(attr => attr is AllowedProcessorsAttribute apAttr
26+
&& apAttr.Processors.Contains(typeof(TextCommandProcessor))));
27+
28+
if (commandSplit.Length != 0 && commandSplit[0] != "")
29+
{
30+
commandSplit[0] += "textcmd";
31+
32+
Command? cmd = null;
33+
IEnumerable<Command>? searchIn = cmds;
34+
foreach (string c in commandSplit)
35+
{
36+
if (searchIn is null)
37+
{
38+
cmd = null;
39+
break;
40+
}
41+
42+
StringComparison comparison = StringComparison.InvariantCultureIgnoreCase;
43+
StringComparer comparer = StringComparer.InvariantCultureIgnoreCase;
44+
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));
45+
46+
if (cmd is null)
47+
{
48+
break;
49+
}
50+
51+
IEnumerable<ContextCheckAttribute> failedChecks = await CheckPermissionsAsync(ctx, cmd);
52+
if (failedChecks.Any())
53+
{
54+
return;
55+
}
56+
57+
searchIn = cmd.Subcommands.Any() ? cmd.Subcommands : null;
58+
}
59+
60+
if (cmd is null)
61+
{
62+
throw new CommandNotFoundException(string.Join(" ", commandSplit));
63+
}
64+
65+
helpEmbed.Description = $"`{cmd.Name.Replace("textcmd", "")}`: {cmd.Description ?? "No description provided."}";
66+
67+
68+
if (cmd.Subcommands.Count > 0 && cmd.Subcommands.Any(subCommand => subCommand.Attributes.Any(attr => attr is DefaultGroupCommandAttribute)))
69+
{
70+
helpEmbed.Description += "\n\nThis group can be executed as a standalone command.";
71+
}
72+
73+
var aliases = cmd.Method?.GetCustomAttributes<TextAliasAttribute>().FirstOrDefault()?.Aliases ?? (cmd.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases ?? null;
74+
if (aliases is not null && aliases.Length > 1)
75+
{
76+
var aliasStr = "";
77+
foreach (var alias in aliases)
78+
{
79+
if (alias == cmd.Name.Replace("textcmd", ""))
80+
continue;
81+
82+
aliasStr += $"`{alias}`, ";
83+
}
84+
aliasStr = aliasStr.TrimEnd(',', ' ');
85+
helpEmbed.AddField("Aliases", aliasStr);
86+
}
87+
88+
var arguments = cmd.Method?.GetParameters();
89+
if (arguments is not null && arguments.Length > 0)
90+
{
91+
var argumentsStr = $"`{cmd.Name.Replace("textcmd", "")}";
92+
foreach (var arg in arguments)
93+
{
94+
if (arg.ParameterType is CommandContext || arg.ParameterType.IsSubclassOf(typeof(CommandContext)))
95+
continue;
96+
97+
bool isCatchAll = arg.GetCustomAttribute<RemainingTextAttribute>() != null;
98+
argumentsStr += $"{(arg.IsOptional || isCatchAll ? " [" : " <")}{arg.Name}{(isCatchAll ? "..." : "")}{(arg.IsOptional || isCatchAll ? "]" : ">")}";
99+
}
100+
101+
argumentsStr += "`\n";
102+
103+
foreach (var arg in arguments)
104+
{
105+
if (arg.ParameterType is CommandContext || arg.ParameterType.IsSubclassOf(typeof(CommandContext)))
106+
continue;
107+
108+
argumentsStr += $"`{arg.Name} ({arg.ParameterType.Name})`: {arg.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "No description provided."}\n";
109+
}
110+
111+
helpEmbed.AddField("Arguments", argumentsStr.Trim());
112+
}
113+
//helpBuilder.WithCommand(cmd);
114+
115+
if (cmd.Subcommands.Any())
116+
{
117+
IEnumerable<Command> commandsToSearch = cmd.Subcommands;
118+
List<Command> eligibleCommands = [];
119+
foreach (Command? candidateCommand in commandsToSearch)
120+
{
121+
var executionChecks = candidateCommand.Attributes.Where(x => x is ContextCheckAttribute) as List<ContextCheckAttribute>;
122+
123+
if (executionChecks == null || !executionChecks.Any())
124+
{
125+
eligibleCommands.Add(candidateCommand);
126+
continue;
127+
}
128+
129+
IEnumerable<ContextCheckAttribute> candidateFailedChecks = await CheckPermissionsAsync(ctx, candidateCommand);
130+
if (!candidateFailedChecks.Any())
131+
{
132+
eligibleCommands.Add(candidateCommand);
133+
}
134+
}
135+
136+
if (eligibleCommands.Count != 0)
137+
{
138+
eligibleCommands = eligibleCommands.OrderBy(x => x.Name).ToList();
139+
string cmdList = "";
140+
foreach (var subCommand in eligibleCommands)
141+
{
142+
cmdList += $"`{subCommand.Name}`, ";
143+
}
144+
helpEmbed.AddField("Subcommands", cmdList.TrimEnd(',', ' '));
145+
//helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
146+
}
147+
}
148+
}
149+
else
150+
{
151+
IEnumerable<Command> commandsToSearch = cmds;
152+
List<Command> eligibleCommands = [];
153+
foreach (Command? sc in commandsToSearch)
154+
{
155+
var executionChecks = sc.Attributes.Where(x => x is ContextCheckAttribute);
156+
157+
if (!executionChecks.Any())
158+
{
159+
eligibleCommands.Add(sc);
160+
continue;
161+
}
162+
163+
IEnumerable<ContextCheckAttribute> candidateFailedChecks = await CheckPermissionsAsync(ctx, sc);
164+
if (!candidateFailedChecks.Any())
165+
{
166+
eligibleCommands.Add(sc);
167+
}
168+
}
169+
170+
if (eligibleCommands.Count != 0)
171+
{
172+
eligibleCommands = eligibleCommands.OrderBy(x => x.Name).ToList();
173+
string cmdList = "";
174+
foreach (var eligibleCommand in eligibleCommands)
175+
{
176+
cmdList += $"`{eligibleCommand.Name.Replace("textcmd", "")}`, ";
177+
}
178+
helpEmbed.AddField("Commands", cmdList.TrimEnd(',', ' '));
179+
helpEmbed.Description = "Listing all top-level commands and groups. Specify a command to see more information.";
180+
//helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
181+
}
182+
}
183+
184+
DiscordMessageBuilder builder = new DiscordMessageBuilder().AddEmbed(helpEmbed);
185+
186+
await ctx.RespondAsync(builder);
187+
}
188+
189+
[Command("pingtextcmd")]
190+
[TextAlias("ping")]
191+
[Description("Pong? This command lets you know whether I'm working well.")]
192+
[AllowedProcessors(typeof(TextCommandProcessor))]
193+
public async Task Ping(TextCommandContext ctx)
194+
{
195+
ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString());
196+
DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging...");
197+
ulong ping = (return_message.Id - ctx.Message.Id) >> 22;
198+
char[] choices = new char[] { 'a', 'e', 'o', 'u', 'i', 'y' };
199+
char letter = choices[Program.rand.Next(0, choices.Length)];
200+
await return_message.ModifyAsync($"P{letter}ng! 🏓\n" +
201+
$"• It took me `{ping}ms` to reply to your message!\n" +
202+
$"• Last Websocket Heartbeat took `{Math.Round(ctx.Client.GetConnectionLatency(0).TotalMilliseconds, 0)}ms`!");
203+
}
204+
205+
[Command("userinfo")]
206+
[TextAlias("user-info", "whois")]
207+
[Description("Show info about a user.")]
208+
[AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))]
209+
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)
210+
{
211+
if (user is null)
212+
user = ctx.User;
213+
214+
await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage);
215+
}
216+
217+
[Command("remindmetextcmd")]
218+
[Description("Set a reminder for yourself. Example: !reminder 1h do the thing")]
219+
[TextAlias("remindme", "reminder", "rember", "wemember", "remember", "remind")]
220+
[AllowedProcessors(typeof(TextCommandProcessor))]
221+
[RequireHomeserverPerm(ServerPermLevel.Tier4, WorkOutside = true)]
222+
public async Task RemindMe(
223+
TextCommandContext ctx,
224+
[Description("The amount of time to wait before reminding you. For example: 2s, 5m, 1h, 1d")] string timetoParse,
225+
[RemainingText, Description("The text to send when the reminder triggers.")] string reminder
226+
)
227+
{
228+
DateTime t = HumanDateParser.HumanDateParser.Parse(timetoParse);
229+
if (t <= DateTime.Now)
230+
{
231+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time can't be in the past!");
232+
return;
233+
}
234+
#if !DEBUG
235+
else if (t < (DateTime.Now + TimeSpan.FromSeconds(59)))
236+
{
237+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time must be at least a minute in the future!");
238+
return;
239+
}
240+
#endif
241+
string guildId;
242+
243+
if (ctx.Channel.IsPrivate)
244+
guildId = "@me";
245+
else
246+
guildId = ctx.Guild.Id.ToString();
247+
248+
var reminderObject = new Reminder()
249+
{
250+
UserID = ctx.User.Id,
251+
ChannelID = ctx.Channel.Id,
252+
MessageID = ctx.Message.Id,
253+
MessageLink = $"https://discord.com/channels/{guildId}/{ctx.Channel.Id}/{ctx.Message.Id}",
254+
ReminderText = reminder,
255+
ReminderTime = t,
256+
OriginalTime = DateTime.Now
257+
};
258+
259+
await Program.db.ListRightPushAsync("reminders", JsonConvert.SerializeObject(reminderObject));
260+
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)}**)");
261+
}
262+
263+
public class Reminder
264+
{
265+
[JsonProperty("userID")]
266+
public ulong UserID { get; set; }
267+
268+
[JsonProperty("channelID")]
269+
public ulong ChannelID { get; set; }
270+
271+
[JsonProperty("messageID")]
272+
public ulong MessageID { get; set; }
273+
274+
[JsonProperty("messageLink")]
275+
public string MessageLink { get; set; }
276+
277+
[JsonProperty("reminderText")]
278+
public string ReminderText { get; set; }
279+
280+
[JsonProperty("reminderTime")]
281+
public DateTime ReminderTime { get; set; }
282+
283+
[JsonProperty("originalTime")]
284+
public DateTime OriginalTime { get; set; }
285+
}
286+
287+
// Runs command context checks manually. Returns a list of failed checks.
288+
// Unfortunately DSharpPlus.Commands does not provide a way to execute a command's context checks manually,
289+
// so this will have to do. This may not include all checks, but it includes everything I could think of. -Milkshake
290+
private async Task<IEnumerable<ContextCheckAttribute>> CheckPermissionsAsync(CommandContext ctx, Command cmd)
291+
{
292+
var contextChecks = cmd.Attributes.Where(x => x is ContextCheckAttribute);
293+
var failedChecks = new List<ContextCheckAttribute>();
294+
295+
foreach (var check in contextChecks)
296+
{
297+
if (check is HomeServerAttribute homeServerAttribute)
298+
{
299+
if (ctx.Channel.IsPrivate || ctx.Guild is null || ctx.Guild.Id != Program.cfgjson.ServerID)
300+
{
301+
failedChecks.Add(homeServerAttribute);
302+
}
303+
}
304+
305+
if (check is RequireHomeserverPermAttribute requireHomeserverPermAttribute)
306+
{
307+
if (ctx.Member is null && !requireHomeserverPermAttribute.WorkOutside)
308+
{
309+
failedChecks.Add(requireHomeserverPermAttribute);
310+
}
311+
else
312+
{
313+
if (!requireHomeserverPermAttribute.WorkOutside)
314+
{
315+
var level = await GetPermLevelAsync(ctx.Member);
316+
if (level < requireHomeserverPermAttribute.TargetLvl)
317+
{
318+
failedChecks.Add(requireHomeserverPermAttribute);
319+
}
320+
}
321+
}
322+
323+
}
324+
325+
if (check is RequirePermissionsAttribute requirePermissionsAttribute)
326+
{
327+
if (ctx.Member is null || ctx.Guild is null
328+
|| !ctx.Channel.PermissionsFor(ctx.Member).HasAllPermissions(requirePermissionsAttribute.UserPermissions)
329+
|| !ctx.Channel.PermissionsFor(ctx.Guild.CurrentMember).HasAllPermissions(requirePermissionsAttribute.BotPermissions))
330+
{
331+
failedChecks.Add(requirePermissionsAttribute);
332+
}
333+
}
334+
335+
if (check is IsBotOwnerAttribute isBotOwnerAttribute)
336+
{
337+
if (!Program.cfgjson.BotOwners.Contains(ctx.User.Id))
338+
{
339+
failedChecks.Add(isBotOwnerAttribute);
340+
}
341+
}
342+
343+
if (check is UserRolesPresentAttribute userRolesPresentAttribute)
344+
{
345+
if (Program.cfgjson.UserRoles is null)
346+
{
347+
failedChecks.Add(userRolesPresentAttribute);
348+
}
349+
}
350+
}
351+
352+
return failedChecks;
353+
}
354+
}
355+
}

0 commit comments

Comments
 (0)