1 |
rakinar2 |
577 |
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 |
|
|
} |