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

Annotation of /branches/5.x/src/core/Command.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (hide annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 28870 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 rakinar2 577 /**
2     * This file is part of SudoBot.
3     *
4     * Copyright (C) 2021-2023 OSN Developers.
5     *
6     * SudoBot is free software; you can redistribute it and/or modify it
7     * under the terms of the GNU Affero General Public License as published by
8     * the Free Software Foundation, either version 3 of the License, or
9     * (at your option) any later version.
10     *
11     * SudoBot is distributed in the hope that it will be useful, but
12     * WITHOUT ANY WARRANTY; without even the implied warranty of
13     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14     * GNU Affero General Public License for more details.
15     *
16     * You should have received a copy of the GNU Affero General Public License
17     * along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18     */
19    
20     import {
21     APIMessage,
22     ApplicationCommandType,
23     CacheType,
24     ChatInputCommandInteraction,
25     ContextMenuCommandBuilder,
26     ContextMenuCommandInteraction,
27     GuildBasedChannel,
28     GuildMember,
29     InteractionDeferReplyOptions,
30     InteractionEditReplyOptions,
31     InteractionReplyOptions,
32     Message,
33     MessageCreateOptions,
34     MessageMentions,
35     MessagePayload,
36     PermissionResolvable,
37     PermissionsBitField,
38     Role,
39     SlashCommandBuilder,
40     Snowflake,
41     User
42     } from "discord.js";
43     import { ChatInputCommandContext, ContextMenuCommandContext, LegacyCommandContext } from "../services/CommandManager";
44     import { stringToTimeInterval } from "../utils/datetime";
45     import { log, logError } from "../utils/logger";
46     import { getEmoji, isSnowflake } from "../utils/utils";
47     import Client from "./Client";
48    
49     export type CommandMessage = Message<boolean> | ChatInputCommandInteraction<CacheType> | ContextMenuCommandInteraction;
50     export type BasicCommandContext = LegacyCommandContext | ChatInputCommandContext;
51     export type AnyCommandContext = BasicCommandContext | ContextMenuCommandContext;
52     export type CommandReturn =
53     | ((MessageCreateOptions | APIMessage | InteractionReplyOptions) & { __reply?: boolean })
54     | undefined
55     | null
56     | void;
57    
58     export enum ArgumentType {
59     String = 1,
60     StringRest,
61     Number,
62     Integer,
63     Float,
64     Boolean,
65     Snowflake,
66     User,
67     GuildMember,
68     Channel,
69     Role,
70     Link,
71     TimeInterval
72     }
73    
74     export interface ValidationRule {
75     types?: readonly ArgumentType[];
76     optional?: boolean;
77     default?: any;
78     requiredErrorMessage?: string;
79     typeErrorMessage?: string;
80     entityNotNullErrorMessage?: string;
81     entityNotNull?: boolean;
82     minValue?: number;
83     maxValue?: number;
84     minMaxErrorMessage?: string;
85     lengthMaxErrorMessage?: string;
86     lengthMax?: number;
87     name?: string;
88     timeMilliseconds?: boolean;
89     }
90    
91     type ValidationRuleAndOutputMap = {
92     [ArgumentType.Boolean]: boolean;
93     [ArgumentType.Channel]: GuildBasedChannel;
94     [ArgumentType.Float]: number;
95     [ArgumentType.Number]: number;
96     [ArgumentType.Integer]: number;
97     [ArgumentType.TimeInterval]: number;
98     [ArgumentType.Link]: string;
99     [ArgumentType.Role]: Role;
100     [ArgumentType.Snowflake]: Snowflake;
101     [ArgumentType.String]: string;
102     [ArgumentType.StringRest]: string;
103     [ArgumentType.User]: User;
104     [ArgumentType.GuildMember]: GuildMember;
105     };
106    
107     type ValidationRuleParsedArg<T extends ValidationRule["types"]> = T extends ReadonlyArray<infer U>
108     ? U extends keyof ValidationRuleAndOutputMap
109     ? ValidationRuleAndOutputMap[U]
110     : never
111     : T extends Array<infer U>
112     ? U extends keyof ValidationRuleAndOutputMap
113     ? ValidationRuleAndOutputMap[U]
114     : never
115     : never;
116    
117     export type ValidationRuleParsedArgs<T extends readonly ValidationRule[]> = {
118     [K in keyof T]: ValidationRuleParsedArg<T[K]["types"]>;
119     };
120    
121     // TODO: Split the logic into separate methods
122    
123     export default abstract class Command {
124     public readonly name: string = "";
125     public group: string = "Default";
126     public readonly aliases: string[] = [];
127    
128     public readonly supportsInteractions: boolean = true;
129     public readonly supportsLegacy: boolean = true;
130    
131     public readonly permissions: PermissionResolvable[] = [];
132     public readonly validationRules: readonly ValidationRule[] = [];
133     public readonly permissionMode: "or" | "and" = "and";
134     public readonly systemAdminOnly: boolean = false;
135    
136     public readonly description?: string;
137     public readonly detailedDescription?: string;
138     public readonly argumentSyntaxes?: string[];
139     public readonly availableOptions?: Record<string, string>;
140     public readonly beta: boolean = false;
141     public readonly since: string = "1.0.0";
142     public readonly botRequiredPermissions: PermissionResolvable[] = [];
143     public readonly slashCommandBuilder?:
144     | Partial<Pick<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">>
145     | Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
146    
147     public readonly applicationCommandType: ApplicationCommandType = ApplicationCommandType.ChatInput;
148     public readonly otherApplicationCommandBuilders: (ContextMenuCommandBuilder | SlashCommandBuilder)[] = [];
149    
150     public readonly subcommands: string[] = [];
151    
152     constructor(protected client: Client) {}
153    
154     abstract execute(message: CommandMessage, context: AnyCommandContext): Promise<CommandReturn>;
155    
156     async deferIfInteraction(message: CommandMessage, options?: InteractionDeferReplyOptions) {
157     if (message instanceof ChatInputCommandInteraction) return await message.deferReply(options).catch(logError);
158     }
159    
160     async deferredReply(
161     message: CommandMessage,
162     options: MessageCreateOptions | MessagePayload | InteractionEditReplyOptions | string
163     ) {
164     if (message instanceof ChatInputCommandInteraction || message instanceof ContextMenuCommandInteraction) {
165     return message.deferred ? await message.editReply(options) : await message.reply(options as any);
166     }
167    
168     return message.reply(options as any);
169     }
170    
171     async error(message: CommandMessage, errorMessage?: string) {
172     return await this.deferredReply(
173     message,
174     errorMessage
175     ? `${this.emoji("error")} ${errorMessage}`
176     : `⚠️ An error has occurred while performing this action. Please make sure that the bot has the required permissions to perform this action.`
177     );
178     }
179    
180     async success(message: CommandMessage, successMessage?: string) {
181     return await this.deferredReply(
182     message,
183     successMessage ? `${this.emoji("check")} ${successMessage}` : `Successfully completed the given task.`
184     );
185     }
186    
187     emoji(name: string) {
188     return getEmoji(this.client, name);
189     }
190    
191     async run(message: CommandMessage, context: AnyCommandContext, checkOnly = false) {
192     const isSystemAdmin = this.client.configManager.systemConfig.system_admins.includes(message.member!.user.id);
193    
194     if (this.systemAdminOnly && !isSystemAdmin) {
195     message
196     .reply({
197     content: `${this.emoji("error")} You don't have permission to run this command.`,
198     ephemeral: true
199     })
200     .catch(logError);
201    
202     return;
203     }
204    
205     if (!isSystemAdmin) {
206     const commandName = this.client.commands.get(context.isLegacy ? context.argv[0] : context.commandName)?.name;
207     const { disabled_commands } = this.client.configManager.systemConfig;
208    
209     if (disabled_commands.includes(commandName ?? "")) {
210     await this.error(message, "This command is disabled.");
211     return;
212     }
213    
214     const { channels, guild } = this.client.configManager.config[message.guildId!]?.disabled_commands ?? {};
215    
216     if (guild && guild.includes(commandName ?? "")) {
217     await this.error(message, "This command is disabled in this server.");
218     return;
219     }
220    
221     if (channels && channels[message.channelId!] && channels[message.channelId!].includes(commandName ?? "")) {
222     await this.error(message, "This command is disabled in this channel.");
223     return;
224     }
225     }
226    
227     const { validationRules, permissions } = this;
228     const parsedArgs = [];
229     const parsedNamedArgs: Record<string, any> = {};
230    
231     if (!isSystemAdmin) {
232     let member: GuildMember = <any>message.member!;
233    
234     if (!(member.permissions as any)?.has) {
235     try {
236     member = await message.guild!.members.fetch(member.user.id);
237    
238     if (!member) {
239     throw new Error("Invalid member");
240     }
241     } catch (e) {
242     logError(e);
243     message
244     .reply({
245     content: `Sorry, I couldn't determine whether you have the enough permissions to perform this action or not. Please contact the bot developer.`,
246     ephemeral: true
247     })
248     .catch(logError);
249     return;
250     }
251     }
252    
253     const mode = this.client.configManager.config[message.guildId!]?.permissions?.mode;
254    
255     if (permissions.length > 0) {
256     const memberBotPermissions = this.client.permissionManager.getMemberPermissions(member);
257     const memberRequiredPermissions = new PermissionsBitField(permissions).toArray();
258    
259     if (this.permissionMode === "and") {
260     for (const permission of permissions) {
261     if (!member.permissions.has(permission, true)) {
262     const mode = this.client.configManager.config[message.guildId!]?.permissions?.mode;
263    
264     if (mode !== "advanced" && mode !== "levels") {
265     await message.reply({
266     content: `${this.emoji("error")} You don't have permission to run this command.`,
267     ephemeral: true
268     });
269    
270     log("Skip");
271    
272     return;
273     }
274    
275     const memberBotPermissions = this.client.permissionManager.getMemberPermissions(member);
276     const memberRequiredPermissions = new PermissionsBitField(permissions).toArray();
277    
278     log("PERMS: ", [...memberBotPermissions.values()]);
279     log("PERMS 2: ", memberRequiredPermissions);
280    
281     for (const memberRequiredPermission of memberRequiredPermissions) {
282     if (!memberBotPermissions.has(memberRequiredPermission)) {
283     await message.reply({
284     content: `${this.emoji("error")} You don't have permission to run this command.`,
285     ephemeral: true
286     });
287    
288     return;
289     }
290     }
291     }
292     }
293     } else
294     orMode: {
295     for (const permission of permissions) {
296     if (member.permissions.has(permission, true)) {
297     break orMode;
298     }
299     }
300    
301     if (mode === "advanced" || mode === "levels") {
302     for (const memberRequiredPermission of memberRequiredPermissions) {
303     if (memberBotPermissions.has(memberRequiredPermission)) {
304     break orMode;
305     }
306     }
307     }
308    
309     await message.reply({
310     content: `${this.emoji("error")} You don't have enough permissions to run this command.`,
311     ephemeral: true
312     });
313    
314     return;
315     }
316     }
317    
318     const permissionOverwrite = this.client.commandManager.permissionOverwrites.get(
319     `${message.guildId!}____${this.name}`
320     );
321    
322     log([...this.client.commandManager.permissionOverwrites.keys()]);
323    
324     errorRootBlock: if (permissionOverwrite) {
325     let userCheckPassed = false;
326     let levelCheckPassed = false;
327     let roleCheckPassed = false;
328     let permissonCheckPassed = false;
329    
330     permissionOverwriteIfBlock: {
331     if (permissionOverwrite.requiredUsers.length > 0) {
332     userCheckPassed = permissionOverwrite.requiredUsers.includes(member.user.id);
333    
334     if (permissionOverwrite.mode === "AND" && !userCheckPassed) {
335     break permissionOverwriteIfBlock;
336     }
337    
338     if (permissionOverwrite.mode === "OR" && userCheckPassed) {
339     log("User check passed [OR]");
340     break errorRootBlock;
341     }
342     }
343    
344     if (mode === "levels" && permissionOverwrite.requiredLevel !== null) {
345     const level = this.client.permissionManager.getMemberPermissionLevel(member);
346     levelCheckPassed = level >= permissionOverwrite.requiredLevel;
347    
348     log("level", level, "<", permissionOverwrite.requiredLevel);
349    
350     if (!levelCheckPassed && permissionOverwrite.mode === "AND") {
351     break permissionOverwriteIfBlock;
352     } else if (levelCheckPassed && permissionOverwrite.mode === "OR") {
353     log("Level check passed [OR]");
354     break errorRootBlock;
355     }
356     }
357    
358     if (permissionOverwrite.requiredPermissions.length > 0) {
359     const requiredPermissions = permissionOverwrite.requiredPermissions as PermissionResolvable[];
360    
361     if (permissionOverwrite.requiredPermissionMode === "OR") {
362     let found = false;
363    
364     for (const permission of requiredPermissions) {
365     if (member.permissions.has(permission, true)) {
366     found = true;
367     break;
368     }
369     }
370    
371     permissonCheckPassed = found;
372    
373     if (!found && permissionOverwrite.mode === "AND") {
374     break permissionOverwriteIfBlock;
375     } else if (found && permissionOverwrite.mode === "OR") {
376     log("Permission check passed [OR_OR]");
377     break errorRootBlock;
378     }
379     } else {
380     log(requiredPermissions);
381     permissonCheckPassed = member.permissions.has(requiredPermissions, true);
382    
383     if (!permissonCheckPassed && permissionOverwrite.mode === "AND") {
384     log("Fail");
385     break permissionOverwriteIfBlock;
386     } else if (permissonCheckPassed && permissionOverwrite.mode === "OR") {
387     log("Permission check passed [AND_OR]");
388     break errorRootBlock;
389     }
390     }
391     }
392    
393     if (permissionOverwrite.requiredRoles.length > 0) {
394     roleCheckPassed = member.roles.cache.hasAll(...permissionOverwrite.requiredRoles);
395    
396     if (!roleCheckPassed && permissionOverwrite.mode === "AND") {
397     break permissionOverwriteIfBlock;
398     } else if (roleCheckPassed && permissionOverwrite.mode === "OR") {
399     break errorRootBlock;
400     }
401     }
402    
403     log("userCheckPassed", userCheckPassed);
404     log("levelCheckPassed", levelCheckPassed);
405     log("permissonCheckPassed", permissonCheckPassed);
406     log("roleCheckPassed", roleCheckPassed);
407    
408     if (permissionOverwrite.mode === "OR") {
409     break permissionOverwriteIfBlock;
410     }
411    
412     if (
413     permissionOverwrite.requiredChannels.length > 0 &&
414     !permissionOverwrite.requiredChannels.includes(message.channelId!)
415     ) {
416     await message.reply({
417     content: `${this.emoji("error")} This command is disabled in this channel.`,
418     ephemeral: true
419     });
420    
421     return;
422     }
423    
424     break errorRootBlock;
425     }
426    
427     await message.reply({
428     content: `${this.emoji("error")} You don't have enough permissions to run this command.`,
429     ephemeral: true
430     });
431    
432     return;
433     }
434     }
435    
436     if (context.isLegacy) {
437     let index = 0;
438    
439     loop: for await (const rule of validationRules) {
440     const arg = context.args[index];
441    
442     if (arg === undefined) {
443     if (!rule.optional) {
444     await this.error(message, rule.requiredErrorMessage ?? `Argument #${index} is required`);
445     return;
446     }
447    
448     if (rule.default !== undefined) {
449     parsedArgs.push(rule.default);
450     }
451    
452     continue;
453     }
454    
455     if (rule.types) {
456     const prevLengthOuter = parsedArgs.length;
457    
458     for (const type of rule.types) {
459     const prevLength = parsedArgs.length;
460    
461     if (
462     /^(\-)?[\d\.]+$/.test(arg) &&
463     (((rule.minValue || rule.maxValue) && type === ArgumentType.Float) ||
464     type === ArgumentType.Integer ||
465     type === ArgumentType.Number)
466     ) {
467     const float = parseFloat(arg);
468    
469     if (
470     !isNaN(float) &&
471     ((rule.minValue !== undefined && rule.minValue > float) ||
472     (rule.maxValue !== undefined && rule.maxValue < float))
473     ) {
474     await message.reply(
475     rule.minMaxErrorMessage ??
476     `Argument #${index} has a min/max numeric value range but the given value is out of range.`
477     );
478     return;
479     }
480     }
481    
482     switch (type) {
483     case ArgumentType.Boolean:
484     if (["true", "false"].includes(arg.toLowerCase())) {
485     parsedArgs[index] = arg.toLowerCase() === "true";
486     }
487    
488     break;
489    
490     case ArgumentType.Float:
491     const float = parseFloat(arg);
492    
493     if (isNaN(float)) {
494     break;
495     }
496    
497     parsedArgs[index] = float;
498     break;
499    
500     case ArgumentType.Integer:
501     if (!/^(\-)?\d+$/.test(arg)) {
502     break;
503     }
504    
505     const int = parseInt(arg);
506    
507     if (isNaN(int)) {
508     break;
509     }
510    
511     parsedArgs[index] = int;
512     break;
513    
514     case ArgumentType.Number:
515     const number = arg.includes(".") ? parseFloat(arg) : parseInt(arg);
516    
517     if (isNaN(number)) {
518     break;
519     }
520    
521     parsedArgs[index] = number;
522     break;
523    
524     case ArgumentType.TimeInterval:
525     const { result, error } = stringToTimeInterval(arg, {
526     milliseconds: rule.timeMilliseconds ?? false
527     });
528    
529     if (error) {
530     if (rule.types.length === 1) {
531     await message
532     .reply({
533     ephemeral: true,
534     content: `${this.emoji("error")} ${error}`
535     })
536     .catch(logError);
537    
538     return;
539     }
540    
541     break;
542     }
543    
544     if (
545     !isNaN(result) &&
546     ((rule.minValue !== undefined && rule.minValue > result) ||
547     (rule.maxValue !== undefined && rule.maxValue < result))
548     ) {
549     await message.reply(
550     `${this.emoji("error")} ` + rule.minMaxErrorMessage ??
551     `Argument #${index} has a min/max numeric time value range but the given value is out of range.`
552     );
553     return;
554     }
555    
556     parsedArgs[index] = result;
557     break;
558    
559     case ArgumentType.Link:
560     try {
561     parsedArgs[index] = new URL(arg);
562     } catch (e) {
563     break;
564     }
565    
566     break;
567    
568     case ArgumentType.String:
569     if (arg.trim() === "") break;
570    
571     parsedArgs[index] = arg;
572     break;
573    
574     case ArgumentType.Snowflake:
575     if (!isSnowflake(arg)) break;
576    
577     parsedArgs[index] = arg;
578     break;
579    
580     case ArgumentType.User:
581     case ArgumentType.GuildMember:
582     case ArgumentType.Channel:
583     case ArgumentType.Role:
584     // TODO: Use message.mentions object to improve performance and reduce API requests
585    
586     let id;
587    
588     if (MessageMentions.UsersPattern.test(arg)) {
589     id = arg.substring(arg.includes("!") ? 3 : 2, arg.length - 1);
590     } else if (MessageMentions.ChannelsPattern.test(arg)) {
591     id = arg.substring(2, arg.length - 1);
592     } else if (MessageMentions.RolesPattern.test(arg)) {
593     id = arg.substring(3, arg.length - 1);
594     } else if (isSnowflake(arg)) {
595     id = arg;
596     } else {
597     break;
598     }
599    
600     try {
601     let entity = null;
602    
603     if (type === ArgumentType.User) entity = await this.client.users.fetch(id);
604     else {
605     entity =
606     type === ArgumentType.Role
607     ? await message.guild!.roles.fetch(id)
608     : type === ArgumentType.Channel
609     ? await message.guild!.channels.fetch(id)
610     : await message.guild!.members.fetch(id);
611     }
612    
613     if (!entity) {
614     throw new Error("Invalid entity received");
615     }
616    
617     parsedArgs[index] = entity;
618     } catch (e) {
619     logError(e);
620    
621     if (rule.entityNotNull) {
622     await message.reply(
623     `${this.emoji("error")} ` + rule.entityNotNullErrorMessage ??
624     `Argument ${index} is invalid`
625     );
626     return;
627     }
628    
629     parsedArgs[index] = null;
630     }
631    
632     break;
633    
634     case ArgumentType.StringRest:
635     if (arg.trim() === "") break;
636    
637     let str = ((message as Message).content ?? "")
638     .slice(context.prefix.length)
639     .trimStart()
640     .slice(context.argv[0].length)
641     .trimStart();
642    
643     for (let i = 0; i < index; i++) {
644     str = str.slice(context.args[i].length).trimStart();
645     }
646    
647     str = str.trimEnd();
648    
649     if (str === "") break;
650    
651     parsedArgs[index] = str;
652    
653     if (rule.name) {
654     parsedNamedArgs[rule.name] = parsedArgs[index];
655     }
656    
657     break loop;
658     }
659    
660     if (
661     rule.lengthMax !== undefined &&
662     typeof parsedArgs[index] === "string" &&
663     parsedArgs[index].length > rule.lengthMax
664     ) {
665     await message.reply(
666     `${this.emoji("error")} ` + rule.lengthMaxErrorMessage ?? `Argument #${index} is too long`
667     );
668     return;
669     }
670    
671     if (prevLength !== parsedArgs.length) {
672     break;
673     }
674     }
675    
676     if (prevLengthOuter === parsedArgs.length) {
677     await message.reply(
678     `${this.emoji("error")} ` + rule.typeErrorMessage ?? `Argument #${index} is invalid, type mismatch`
679     );
680     return;
681     }
682     }
683    
684     if (rule.name) {
685     parsedNamedArgs[rule.name] = parsedArgs[index];
686     }
687    
688     index++;
689     }
690     }
691    
692     if (!checkOnly) {
693     return await this.execute(message, {
694     ...context,
695     ...(context.isLegacy
696     ? {
697     parsedArgs,
698     parsedNamedArgs
699     }
700     : {})
701     });
702     }
703     }
704     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26