/[sudobot]/branches/6.x/src/core/Command.ts
ViewVC logotype

Contents of /branches/6.x/src/core/Command.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 17737 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 import {
2 APIEmbed,
3 APIEmbedField,
4 APIMessage,
5 ApplicationCommandType,
6 Awaitable,
7 CacheType,
8 Channel,
9 ChatInputCommandInteraction,
10 ContextMenuCommandBuilder,
11 ContextMenuCommandInteraction,
12 GuildMember,
13 InteractionDeferReplyOptions,
14 InteractionEditReplyOptions,
15 InteractionReplyOptions,
16 Message,
17 MessageCreateOptions,
18 MessagePayload,
19 ModalSubmitInteraction,
20 PermissionResolvable,
21 SlashCommandBuilder,
22 TextBasedChannel,
23 User
24 } from "discord.js";
25 import { ChatInputCommandContext, ContextMenuCommandContext, LegacyCommandContext } from "../services/CommandManager";
26 import EmbedSchemaParser from "../utils/EmbedSchemaParser";
27 import { channelInfo, guildInfo, userInfo } from "../utils/embed";
28 import { safeChannelFetch } from "../utils/fetch";
29 import { logError } from "../utils/logger";
30 import { getEmoji } from "../utils/utils";
31 import Client from "./Client";
32 import CommandArgumentParser from "./CommandArgumentParser";
33 import { ValidationRule } from "./CommandArgumentParserInterface";
34
35 export * from "./CommandArgumentParserInterface";
36
37 export type CommandMessage = Message<boolean> | ChatInputCommandInteraction<CacheType> | ContextMenuCommandInteraction;
38 export type BasicCommandContext = LegacyCommandContext | ChatInputCommandContext;
39 export type AnyCommandContext = BasicCommandContext | ContextMenuCommandContext;
40 export type CommandReturn =
41 | ((MessageCreateOptions | APIMessage | InteractionReplyOptions) & { __reply?: boolean })
42 | undefined
43 | null
44 | void;
45
46 type CommandSlashCommandBuilder =
47 | Partial<Pick<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">>
48 | Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
49
50 type DeferReplyMode = "delete" | "channel" | "default" | "auto";
51 type DeferReplyOptions =
52 | ((MessageCreateOptions | MessagePayload | InteractionEditReplyOptions) & { ephemeral?: boolean })
53 | string;
54 type PermissionValidationResult =
55 | boolean
56 | {
57 isPermitted: boolean;
58 source?: string;
59 };
60 export type RunCommandOptions = {
61 message: CommandMessage;
62 context: AnyCommandContext;
63 onAbort?: () => unknown;
64 checkOnly?: boolean;
65 };
66
67 class PermissionError extends Error {}
68
69 export default abstract class Command {
70 public readonly name: string = "";
71 public group: string = "Default";
72 public readonly aliases: string[] = [];
73
74 public readonly supportsInteractions: boolean = true;
75 public readonly supportsLegacy: boolean = true;
76
77 public readonly permissions: PermissionResolvable[] = [];
78 public readonly validationRules: readonly ValidationRule[] = [];
79 public readonly permissionMode: "or" | "and" = "and";
80 public readonly systemAdminOnly: boolean = false;
81
82 public readonly description?: string;
83 public readonly detailedDescription?: string;
84 public readonly argumentSyntaxes?: string[];
85 public readonly availableOptions?: Record<string, string>;
86 public readonly beta: boolean = false;
87 public readonly since: string = "1.0.0";
88 public readonly botRequiredPermissions: PermissionResolvable[] = [];
89 public readonly slashCommandBuilder?: CommandSlashCommandBuilder;
90
91 public readonly applicationCommandType: ApplicationCommandType = ApplicationCommandType.ChatInput;
92 public readonly otherApplicationCommandBuilders: (ContextMenuCommandBuilder | SlashCommandBuilder)[] = [];
93
94 public readonly subcommands: string[] = [];
95 public readonly subCommandCheck: boolean = false;
96 public readonly cooldown?: number = undefined;
97
98 protected message?: CommandMessage;
99
100 protected static argumentParser = new CommandArgumentParser(Client.instance);
101
102 protected constructor(protected client: Client<true>) {}
103
104 abstract execute(message: CommandMessage, context: AnyCommandContext, options?: RunCommandOptions): Promise<CommandReturn>;
105
106 protected getCommandMessage(): CommandMessage {
107 if (!this.message) {
108 throw new TypeError(`${this.constructor.name}.message is undefined`);
109 }
110
111 return this.message;
112 }
113
114 protected async deferIfInteraction(message: CommandMessage, options?: InteractionDeferReplyOptions) {
115 if (message instanceof ChatInputCommandInteraction) return await message.deferReply(options).catch(logError);
116 }
117
118 public async deferredReply(message: CommandMessage, options: DeferReplyOptions, mode: DeferReplyMode = "default") {
119 if (message instanceof ChatInputCommandInteraction || message instanceof ContextMenuCommandInteraction) {
120 return message.deferred ? await message.editReply(options) : await message.reply(options as any);
121 }
122
123 const behaviour = this.client.configManager.config[message.guildId!]?.commands.moderation_command_behaviour;
124
125 if (mode === "delete" || (mode === "auto" && behaviour === "delete")) {
126 await message.delete().catch(logError);
127 }
128
129 return mode === "delete" || (mode === "auto" && behaviour === "delete") || mode === "channel"
130 ? message.channel.send(options as any)
131 : message.reply(options as any);
132 }
133
134 protected async error(message: CommandMessage, errorMessage?: string, mode: DeferReplyMode = "default") {
135 return await this.deferredReply(
136 message,
137 errorMessage
138 ? `${this.emoji("error")} ${errorMessage}`
139 : `⚠️ An error has occurred while performing this action. Please make sure that the bot has the required permissions to perform this action.`,
140 mode
141 );
142 }
143
144 protected async success(message: CommandMessage, successMessage?: string, mode: DeferReplyMode = "default") {
145 return await this.deferredReply(
146 message,
147 successMessage ? `${this.emoji("check")} ${successMessage}` : `Successfully completed the given task.`,
148 mode
149 );
150 }
151
152 protected sendError(errorMessage?: string, mode: DeferReplyMode = "default") {
153 return this.error(this.getCommandMessage(), errorMessage, mode);
154 }
155
156 protected sendSuccess(successMessage?: string, mode: DeferReplyMode = "default") {
157 return this.success(this.getCommandMessage(), successMessage, mode);
158 }
159
160 protected emoji(name: string) {
161 return getEmoji(this.client, name);
162 }
163
164 protected async sendCommandRanLog(
165 message: CommandMessage | ModalSubmitInteraction,
166 options: APIEmbed,
167 params: {
168 fields?: (fields: APIEmbedField[]) => Awaitable<APIEmbedField[]>;
169 before?: (channel: TextBasedChannel, sentMessages: Array<Message | null>) => any;
170 previews?: Array<MessageCreateOptions | MessagePayload>;
171 url?: string | null;
172 } = {}
173 ) {
174 if (!this.client.configManager.systemConfig.logging?.enabled) {
175 return;
176 }
177
178 const { previews = [] } = params;
179 const logChannelId = this.client.configManager.systemConfig.logging?.channels?.echo_send_logs;
180
181 if (!logChannelId) {
182 return;
183 }
184
185 try {
186 const channel = await safeChannelFetch(await this.client.getHomeGuild(), logChannelId);
187
188 if (!channel?.isTextBased()) {
189 return;
190 }
191
192 const sentMessages = [];
193
194 for (const preview of previews) {
195 const sentMessage = await EmbedSchemaParser.sendMessage(channel, {
196 ...preview,
197 reply: undefined
198 } as MessageCreateOptions).catch(logError);
199 sentMessages.push(sentMessage ?? null);
200 }
201
202 (await params.before?.(channel, sentMessages))?.catch?.(logError);
203
204 const embedFields = [
205 {
206 name: "Command Name",
207 value: this.name
208 },
209 {
210 name: "Guild Info",
211 value: guildInfo(message.guild!),
212 inline: true
213 },
214 {
215 name: "Channel Info",
216 value: channelInfo(message.channel as Channel),
217 inline: true
218 },
219 {
220 name: "User (Executor)",
221 value: userInfo(message.member!.user as User),
222 inline: true
223 },
224 {
225 name: "Mode",
226 value: message instanceof Message ? "Legacy" : "Application Interaction"
227 },
228 ...(params.url !== null
229 ? [
230 {
231 name: "Message URL",
232 value: params.url ?? (message instanceof Message ? message.url : "*Not available*")
233 }
234 ]
235 : [])
236 ];
237
238 await channel
239 ?.send({
240 embeds: [
241 {
242 author: {
243 name: message.member?.user.username as string,
244 icon_url: (message.member?.user as User)?.displayAvatarURL()
245 },
246 title: "A command was executed",
247 color: 0x007bff,
248 fields: (await params.fields?.(embedFields)) ?? embedFields,
249 ...options
250 }
251 ]
252 })
253 .catch(logError);
254 } catch (error) {
255 logError(error);
256 }
257 }
258
259 /**
260 * Check for command cooldowns.
261 *
262 * @param message The target message
263 * @returns {Promise<boolean>} Whether to abort the command
264 */
265 private async cooldownCheck(message: CommandMessage): Promise<boolean> {
266 if (!this.cooldown) {
267 return false;
268 }
269
270 const { cooldown, enabled } = this.client.cooldown.lock(message.guildId!, this.name, this.cooldown);
271
272 if (enabled) {
273 const seconds = Math.max(Math.ceil(((cooldown ?? 0) - Date.now()) / 1000), 1);
274
275 await this.deferredReply(message, {
276 embeds: [
277 {
278 description: `${this.emoji(
279 "clock_red"
280 )} You're being rate limited, please wait for **${seconds}** second${seconds === 1 ? "" : "s"}.`,
281 color: 0xf14a60
282 }
283 ],
284 ephemeral: true
285 });
286
287 return true;
288 }
289
290 return false;
291 }
292
293 protected async validateCommandBuiltInPermissions(member: GuildMember, hasOverwrite: boolean): Promise<boolean> {
294 if (
295 this.client.configManager.systemConfig.default_permissions_mode === "ignore" ||
296 (hasOverwrite && this.client.configManager.systemConfig.default_permissions_mode === "overwrite")
297 ) {
298 return true;
299 }
300
301 return (
302 (this.permissionMode === "and" && member.permissions.has(this.permissions, true)) ||
303 (this.permissionMode === "or" && member.permissions.any(this.permissions, true))
304 );
305 }
306
307 protected async validateNonSystemAdminPermissions({ message }: RunCommandOptions) {
308 const commandName = this.name;
309 const { disabled_commands } = this.client.configManager.systemConfig;
310
311 if (disabled_commands.includes(commandName ?? "")) {
312 throw new PermissionError("This command is disabled.");
313 }
314
315 const { channels, guild } = this.client.configManager.config[message.guildId!]?.disabled_commands ?? {};
316
317 if (guild && guild.includes(commandName ?? "")) {
318 throw new PermissionError("This command is disabled in this server.");
319 }
320
321 if (channels && channels[message.channelId!] && channels[message.channelId!].includes(commandName ?? "")) {
322 throw new PermissionError("This command is disabled in this channel.");
323 }
324
325 return true;
326 }
327
328 protected validateSystemAdminPermissions(member: GuildMember) {
329 const isSystemAdmin = this.client.configManager.systemConfig.system_admins.includes(member.id);
330
331 if (this.systemAdminOnly && !isSystemAdmin) {
332 throw new PermissionError("This command is only available to system administrators.");
333 }
334
335 return {
336 isSystemAdmin
337 };
338 }
339
340 protected async validatePermissions(
341 options: RunCommandOptions
342 ): Promise<{ result?: PermissionValidationResult; abort?: boolean; error?: string }> {
343 const { message } = options;
344
345 try {
346 const { isSystemAdmin } = this.validateSystemAdminPermissions(message.member as GuildMember);
347
348 if (isSystemAdmin) {
349 return { result: true };
350 }
351
352 const builtInValidationResult = await this.validateCommandBuiltInPermissions(
353 message.member as GuildMember,
354 !!this.client.commandPermissionOverwriteManager.permissionOverwrites.get(`${message.guildId!}____${this.name}`)
355 ?.length
356 );
357
358 if (!builtInValidationResult) {
359 return { result: false };
360 }
361
362 const { result: permissionOverwriteResult } =
363 await this.client.commandPermissionOverwriteManager.validatePermissionOverwrites({
364 commandName: this.name,
365 guildId: message.guildId!,
366 member: message.member as GuildMember,
367 channelId: message.channelId!
368 });
369
370 if (!permissionOverwriteResult) {
371 return {
372 error: "You don't have enough permissions to run this command."
373 };
374 }
375
376 return {
377 result: await this.validateNonSystemAdminPermissions(options)
378 };
379 } catch (error) {
380 if (error instanceof PermissionError) {
381 return {
382 error: error.message
383 };
384 }
385
386 logError(error);
387
388 return {
389 abort: true
390 };
391 }
392 }
393
394 protected async doChecks(options: RunCommandOptions) {
395 const { message } = options;
396 const { result: permissionValidationResult, abort, error } = await this.validatePermissions(options);
397
398 if (abort) {
399 return;
400 }
401
402 const isPermitted =
403 (typeof permissionValidationResult === "boolean" && permissionValidationResult) ||
404 (typeof permissionValidationResult === "object" && permissionValidationResult.isPermitted);
405
406 if (!isPermitted || error) {
407 const permissionValidationFailureSource =
408 (typeof permissionValidationResult === "object" && permissionValidationResult.source) || undefined;
409 const debugMode =
410 (this.client.configManager.systemConfig.debug_mode ||
411 this.client.configManager.config[message.guildId!]?.debug_mode) ??
412 false;
413
414 await this.error(
415 message,
416 `${error ?? "You don't have permission to run this command."}${
417 debugMode && permissionValidationFailureSource ? `\nSource: \`${permissionValidationFailureSource}\`` : ""
418 }`
419 );
420
421 return false;
422 }
423
424 if (this.cooldown) {
425 const abort = await this.cooldownCheck(message);
426
427 if (abort) {
428 return;
429 }
430 }
431
432 return true;
433 }
434
435 protected async parseArguments({ message, context }: RunCommandOptions) {
436 if (!(message instanceof Message) || !context.isLegacy) {
437 return true;
438 }
439
440 const { error, parsedArgs } = await Command.argumentParser.parse({
441 message,
442 input: message.content,
443 prefix: context.prefix,
444 rules: this.validationRules
445 });
446
447 if (error) {
448 await this.error(message, error);
449 return false;
450 }
451
452 if (context.isLegacy && this.subCommandCheck && !this.subcommands.includes(context.args[0])) {
453 await this.error(
454 message,
455 `Please provide a valid subcommand! The valid subcommands are \`${this.subcommands.join("`, `")}\`.`
456 );
457
458 return false;
459 }
460
461 return parsedArgs;
462 }
463
464 async run(options: RunCommandOptions) {
465 const { message, context, checkOnly = false, onAbort } = options;
466
467 this.message = message;
468
469 if (!(await this.doChecks(options))) {
470 this.message = undefined;
471 onAbort?.();
472 return;
473 }
474
475 const parsedArgs = await this.parseArguments(options);
476
477 if (parsedArgs === false) {
478 this.message = undefined;
479 onAbort?.();
480 return;
481 }
482
483 if (checkOnly) {
484 this.message = undefined;
485 return;
486 }
487
488 if (typeof parsedArgs === "object" && context.isLegacy) {
489 context.parsedArgs = [];
490
491 for (const key in parsedArgs) {
492 context.parsedArgs[key as unknown as number] = parsedArgs[key];
493 }
494
495 context.parsedNamedArgs = parsedArgs;
496 }
497
498 const commandReturn = await this.execute(message, context);
499 this.message = undefined;
500 return commandReturn;
501 }
502 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26