feat: inherit botPermissions from the parent command

This commit is contained in:
MARCROCK22 2024-05-31 22:14:34 +00:00
parent 417e650bd4
commit baad784477

View File

@ -1,384 +1,385 @@
import { import {
type APIApplicationCommandOption, type APIApplicationCommandOption,
Locale, Locale,
type LocaleString, type LocaleString,
ApplicationCommandOptionType, ApplicationCommandOptionType,
type APIApplicationCommandIntegerOption, type APIApplicationCommandIntegerOption,
type APIApplicationCommandStringOption, type APIApplicationCommandStringOption,
type APIApplicationCommandSubcommandOption, type APIApplicationCommandSubcommandOption,
type APIApplicationCommandSubcommandGroupOption, type APIApplicationCommandSubcommandGroupOption,
type APIApplicationCommandChannelOption, type APIApplicationCommandChannelOption,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import { basename, dirname } from 'node:path'; import { basename, dirname } from 'node:path';
import type { Logger } from '../common'; import type { Logger } from '../common';
import { BaseHandler } from '../common'; import { BaseHandler } from '../common';
import { Command, SubCommand } from './applications/chat'; import { Command, SubCommand } from './applications/chat';
import { ContextMenuCommand } from './applications/menu'; import { ContextMenuCommand } from './applications/menu';
import type { UsingClient } from './applications/shared'; import type { UsingClient } from './applications/shared';
import { promises } from 'node:fs'; import { promises } from 'node:fs';
export class CommandHandler extends BaseHandler { export class CommandHandler extends BaseHandler {
values: (Command | ContextMenuCommand)[] = []; values: (Command | ContextMenuCommand)[] = [];
protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts')); protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts'));
constructor( constructor(
protected logger: Logger, protected logger: Logger,
protected client: UsingClient, protected client: UsingClient,
) { ) {
super(logger); super(logger);
} }
async reload(resolve: string | Command) { async reload(resolve: string | Command) {
if (typeof resolve === 'string') { if (typeof resolve === 'string') {
return this.values.find(x => x.name === resolve)?.reload(); return this.values.find(x => x.name === resolve)?.reload();
} }
return resolve.reload(); return resolve.reload();
} }
async reloadAll(stopIfFail = true) { async reloadAll(stopIfFail = true) {
for (const command of this.values) { for (const command of this.values) {
try { try {
await this.reload(command.name); await this.reload(command.name);
} catch (e) { } catch (e) {
if (stopIfFail) { if (stopIfFail) {
throw e; throw e;
} }
} }
} }
} }
protected shouldUploadOption(option: APIApplicationCommandOption, cached: APIApplicationCommandOption) { protected shouldUploadOption(option: APIApplicationCommandOption, cached: APIApplicationCommandOption) {
if (option.description !== cached.description) return true; if (option.description !== cached.description) return true;
if (option.type !== cached.type) return true; if (option.type !== cached.type) return true;
if (option.required !== cached.required) return true; if (option.required !== cached.required) return true;
if (option.name !== cached.name) return true; if (option.name !== cached.name) return true;
//TODO: locales //TODO: locales
switch (option.type) { switch (option.type) {
case ApplicationCommandOptionType.String: case ApplicationCommandOptionType.String:
return ( return (
option.min_length !== (cached as APIApplicationCommandStringOption).min_length || option.min_length !== (cached as APIApplicationCommandStringOption).min_length ||
option.max_length !== (cached as APIApplicationCommandStringOption).max_length option.max_length !== (cached as APIApplicationCommandStringOption).max_length
); );
case ApplicationCommandOptionType.Channel: case ApplicationCommandOptionType.Channel:
{ {
if (option.channel_types?.length !== (cached as APIApplicationCommandChannelOption).channel_types?.length) if (option.channel_types?.length !== (cached as APIApplicationCommandChannelOption).channel_types?.length)
return true; return true;
if ('channel_types' in option && 'channel_types' in cached) { if ('channel_types' in option && 'channel_types' in cached) {
if (!(option.channel_types && cached.channel_types)) return true; if (!(option.channel_types && cached.channel_types)) return true;
return option.channel_types.some(ct => !cached.channel_types!.includes(ct)); return option.channel_types.some(ct => !cached.channel_types!.includes(ct));
} }
} }
return; return;
case ApplicationCommandOptionType.Subcommand: case ApplicationCommandOptionType.Subcommand:
case ApplicationCommandOptionType.SubcommandGroup: case ApplicationCommandOptionType.SubcommandGroup:
if ( if (
option.options?.length !== option.options?.length !==
(cached as APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption).options?.length (cached as APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption).options?.length
) { ) {
return true; return true;
} }
if ( if (
option.options && option.options &&
(cached as APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption).options (cached as APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption).options
) )
for (const i of option.options) { for (const i of option.options) {
const cachedOption = ( const cachedOption = (
cached as APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption cached as APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption
).options!.find(x => x.name === i.name); ).options!.find(x => x.name === i.name);
if (!cachedOption) return true; if (!cachedOption) return true;
if (this.shouldUploadOption(i, cachedOption)) return true; if (this.shouldUploadOption(i, cachedOption)) return true;
} }
break; break;
case ApplicationCommandOptionType.Integer: case ApplicationCommandOptionType.Integer:
case ApplicationCommandOptionType.Number: case ApplicationCommandOptionType.Number:
return ( return (
option.min_value !== (cached as APIApplicationCommandIntegerOption).min_value || option.min_value !== (cached as APIApplicationCommandIntegerOption).min_value ||
option.max_value !== (cached as APIApplicationCommandIntegerOption).max_value option.max_value !== (cached as APIApplicationCommandIntegerOption).max_value
); );
case ApplicationCommandOptionType.Attachment: case ApplicationCommandOptionType.Attachment:
case ApplicationCommandOptionType.Boolean: case ApplicationCommandOptionType.Boolean:
case ApplicationCommandOptionType.Mentionable: case ApplicationCommandOptionType.Mentionable:
case ApplicationCommandOptionType.Role: case ApplicationCommandOptionType.Role:
case ApplicationCommandOptionType.User: case ApplicationCommandOptionType.User:
break; break;
} }
return false; return false;
} }
async shouldUpload(file: string) { async shouldUpload(file: string) {
if ( if (
!(await promises.access(file).then( !(await promises.access(file).then(
() => true, () => true,
() => false, () => false,
)) ))
) { ) {
await promises.writeFile(file, JSON.stringify(this.values.map(x => x.toJSON()))); await promises.writeFile(file, JSON.stringify(this.values.map(x => x.toJSON())));
return true; return true;
} }
const cachedCommands: (ReturnType<Command['toJSON']> | ReturnType<ContextMenuCommand['toJSON']>)[] = JSON.parse( const cachedCommands: (ReturnType<Command['toJSON']> | ReturnType<ContextMenuCommand['toJSON']>)[] = JSON.parse(
(await promises.readFile(file)).toString(), (await promises.readFile(file)).toString(),
); );
if (cachedCommands.length !== this.values.length) return true; if (cachedCommands.length !== this.values.length) return true;
for (const command of this.values.map(x => x.toJSON())) { for (const command of this.values.map(x => x.toJSON())) {
const cached = cachedCommands.find(x => { const cached = cachedCommands.find(x => {
if (x.name !== command.name) return false; if (x.name !== command.name) return false;
if (command.guild_id) return command.guild_id.every(id => x.guild_id?.includes(id)); if (command.guild_id) return command.guild_id.every(id => x.guild_id?.includes(id));
return true; return true;
}); });
if (!cached) return true; if (!cached) return true;
if (cached.description !== command.description) return true; if (cached.description !== command.description) return true;
if (cached.default_member_permissions !== command.default_member_permissions) return true; if (cached.default_member_permissions !== command.default_member_permissions) return true;
if (cached.type !== command.type) return true; if (cached.type !== command.type) return true;
if (cached.nsfw !== command.nsfw) return true; if (cached.nsfw !== command.nsfw) return true;
if (!!('options' in cached) !== !!('options' in command)) return true; if (!!('options' in cached) !== !!('options' in command)) return true;
if (!!cached.contexts !== !!command.contexts) return true; if (!!cached.contexts !== !!command.contexts) return true;
if (!!cached.integration_types !== !!command.integration_types) return true; if (!!cached.integration_types !== !!command.integration_types) return true;
//TODO: locales //TODO: locales
if ('contexts' in command && 'contexts' in cached) { if ('contexts' in command && 'contexts' in cached) {
if (command.contexts?.length !== cached.contexts?.length) return true; if (command.contexts?.length !== cached.contexts?.length) return true;
if (command.contexts && cached.contexts) { if (command.contexts && cached.contexts) {
if (command.contexts.some(ctx => !cached.contexts!.includes(ctx))) return true; if (command.contexts.some(ctx => !cached.contexts!.includes(ctx))) return true;
} }
} }
if ('integration_types' in command && 'integration_types' in cached) { if ('integration_types' in command && 'integration_types' in cached) {
if (command.integration_types?.length !== cached.integration_types?.length) return true; if (command.integration_types?.length !== cached.integration_types?.length) return true;
if (command.integration_types && cached.integration_types) { if (command.integration_types && cached.integration_types) {
if (command.integration_types.some(ctx => !cached.integration_types!.includes(ctx))) return true; if (command.integration_types.some(ctx => !cached.integration_types!.includes(ctx))) return true;
} }
} }
if ('options' in command && 'options' in cached) { if ('options' in command && 'options' in cached) {
if (command.options.length !== cached.options.length) return true; if (command.options.length !== cached.options.length) return true;
for (const option of command.options) { for (const option of command.options) {
const cachedOption = cached.options.find(x => x.name === option.name); const cachedOption = cached.options.find(x => x.name === option.name);
if (!cachedOption) return true; if (!cachedOption) return true;
if (this.shouldUploadOption(option, cachedOption)) return true; if (this.shouldUploadOption(option, cachedOption)) return true;
} }
} }
} }
return false; return false;
} }
async load(commandsDir: string, client: UsingClient, instances?: { new (): Command | ContextMenuCommand }[]) { async load(commandsDir: string, client: UsingClient, instances?: { new (): Command | ContextMenuCommand }[]) {
const result = const result =
instances?.map(x => { instances?.map(x => {
const i = new x(); const i = new x();
return { name: i.name, file: x, path: i.__filePath ?? '*' }; return { name: i.name, file: x, path: i.__filePath ?? '*' };
}) ?? }) ??
( (
await this.loadFilesK<{ new (): Command | SubCommand | ContextMenuCommand }>(await this.getFiles(commandsDir)) await this.loadFilesK<{ new (): Command | SubCommand | ContextMenuCommand }>(await this.getFiles(commandsDir))
).filter(x => x.file); ).filter(x => x.file);
this.values = []; this.values = [];
for (const command of result) { for (const command of result) {
let commandInstance; let commandInstance;
try { try {
commandInstance = this.onCommand(command.file); commandInstance = this.onCommand(command.file);
if (!commandInstance) continue; if (!commandInstance) continue;
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.includes('is not a constructor')) { if (e instanceof Error && e.message.includes('is not a constructor')) {
this.logger.warn( this.logger.warn(
`${command.path `${command.path
.split(process.cwd()) .split(process.cwd())
.slice(1) .slice(1)
.join(process.cwd())} doesn't export the class by \`export default <Command>\``, .join(process.cwd())} doesn't export the class by \`export default <Command>\``,
); );
} else this.logger.warn(e, command); } else this.logger.warn(e, command);
continue; continue;
} }
if (commandInstance instanceof ContextMenuCommand) { if (commandInstance instanceof ContextMenuCommand) {
this.values.push(commandInstance); this.values.push(commandInstance);
commandInstance.__filePath = command.path; commandInstance.__filePath = command.path;
this.__parseCommandLocales(commandInstance); this.__parseCommandLocales(commandInstance);
continue; continue;
} }
if (!(commandInstance instanceof Command)) { if (!(commandInstance instanceof Command)) {
continue; continue;
} }
commandInstance.onAfterRun ??= client.options?.commands?.defaults?.onAfterRun; commandInstance.onAfterRun ??= client.options?.commands?.defaults?.onAfterRun;
commandInstance.onBotPermissionsFail ??= client.options?.commands?.defaults?.onBotPermissionsFail; commandInstance.onBotPermissionsFail ??= client.options?.commands?.defaults?.onBotPermissionsFail;
commandInstance.onInternalError ??= client.options?.commands?.defaults?.onInternalError; commandInstance.onInternalError ??= client.options?.commands?.defaults?.onInternalError;
commandInstance.onMiddlewaresError ??= client.options?.commands?.defaults?.onMiddlewaresError; commandInstance.onMiddlewaresError ??= client.options?.commands?.defaults?.onMiddlewaresError;
commandInstance.onOptionsError ??= client.options?.commands?.defaults?.onOptionsError; commandInstance.onOptionsError ??= client.options?.commands?.defaults?.onOptionsError;
commandInstance.onPermissionsFail ??= client.options?.commands?.defaults?.onPermissionsFail; commandInstance.onPermissionsFail ??= client.options?.commands?.defaults?.onPermissionsFail;
commandInstance.onRunError ??= client.options?.commands?.defaults?.onRunError; commandInstance.onRunError ??= client.options?.commands?.defaults?.onRunError;
commandInstance.__filePath = command.path; commandInstance.__filePath = command.path;
commandInstance.options ??= [] as NonNullable<Command['options']>; commandInstance.options ??= [] as NonNullable<Command['options']>;
if (commandInstance.__autoload) { if (commandInstance.__autoload) {
//@AutoLoad //@AutoLoad
const options = await this.getFiles(dirname(command.path)); const options = await this.getFiles(dirname(command.path));
for (const option of options) { for (const option of options) {
if (command.name === basename(option)) { if (command.name === basename(option)) {
continue; continue;
} }
try { try {
const subCommand = this.onSubCommand(result.find(x => x.path === option)!.file as { new (): SubCommand }); const subCommand = this.onSubCommand(result.find(x => x.path === option)!.file as { new (): SubCommand });
if (subCommand && subCommand instanceof SubCommand) { if (subCommand && subCommand instanceof SubCommand) {
subCommand.__filePath = option; subCommand.__filePath = option;
commandInstance.options.push(subCommand); commandInstance.options.push(subCommand);
} }
} catch { } catch {
//pass //pass
} }
} }
} }
for (const option of commandInstance.options ?? []) { for (const option of commandInstance.options ?? []) {
if (option instanceof SubCommand) { if (option instanceof SubCommand) {
option.middlewares = (commandInstance.middlewares ?? []).concat(option.middlewares ?? []); option.middlewares = (commandInstance.middlewares ?? []).concat(option.middlewares ?? []);
option.onMiddlewaresError = option.onMiddlewaresError =
option.onMiddlewaresError?.bind(option) ?? option.onMiddlewaresError?.bind(option) ??
commandInstance.onMiddlewaresError?.bind(commandInstance) ?? commandInstance.onMiddlewaresError?.bind(commandInstance) ??
this.client.options?.commands?.defaults?.onMiddlewaresError; this.client.options?.commands?.defaults?.onMiddlewaresError;
option.onRunError = option.onRunError =
option.onRunError?.bind(option) ?? option.onRunError?.bind(option) ??
commandInstance.onRunError?.bind(commandInstance) ?? commandInstance.onRunError?.bind(commandInstance) ??
this.client.options?.commands?.defaults?.onRunError; this.client.options?.commands?.defaults?.onRunError;
option.onOptionsError = option.onOptionsError =
option.onOptionsError?.bind(option) ?? option.onOptionsError?.bind(option) ??
commandInstance.onOptionsError?.bind(commandInstance) ?? commandInstance.onOptionsError?.bind(commandInstance) ??
this.client.options?.commands?.defaults?.onOptionsError; this.client.options?.commands?.defaults?.onOptionsError;
option.onInternalError = option.onInternalError =
option.onInternalError?.bind(option) ?? option.onInternalError?.bind(option) ??
commandInstance.onInternalError?.bind(commandInstance) ?? commandInstance.onInternalError?.bind(commandInstance) ??
this.client.options?.commands?.defaults?.onInternalError; this.client.options?.commands?.defaults?.onInternalError;
option.onAfterRun = option.onAfterRun =
option.onAfterRun?.bind(option) ?? option.onAfterRun?.bind(option) ??
commandInstance.onAfterRun?.bind(commandInstance) ?? commandInstance.onAfterRun?.bind(commandInstance) ??
this.client.options?.commands?.defaults?.onAfterRun; this.client.options?.commands?.defaults?.onAfterRun;
option.onBotPermissionsFail = option.onBotPermissionsFail =
option.onBotPermissionsFail?.bind(option) ?? option.onBotPermissionsFail?.bind(option) ??
commandInstance.onBotPermissionsFail?.bind(commandInstance) ?? commandInstance.onBotPermissionsFail?.bind(commandInstance) ??
this.client.options?.commands?.defaults?.onBotPermissionsFail; this.client.options?.commands?.defaults?.onBotPermissionsFail;
option.onPermissionsFail = option.onPermissionsFail =
option.onPermissionsFail?.bind(option) ?? option.onPermissionsFail?.bind(option) ??
commandInstance.onPermissionsFail?.bind(commandInstance) ?? commandInstance.onPermissionsFail?.bind(commandInstance) ??
this.client.options?.commands?.defaults?.onPermissionsFail; this.client.options?.commands?.defaults?.onPermissionsFail;
} option.botPermissions ??= commandInstance.botPermissions;
} }
}
this.values.push(commandInstance);
this.__parseCommandLocales(commandInstance); this.values.push(commandInstance);
this.__parseCommandLocales(commandInstance);
for (const i of commandInstance.options ?? []) {
if (i instanceof SubCommand) { for (const i of commandInstance.options ?? []) {
this.__parseCommandLocales(i); if (i instanceof SubCommand) {
} this.__parseCommandLocales(i);
} }
} }
}
return this.values;
} return this.values;
}
private __parseCommandLocales(command: Command | SubCommand | ContextMenuCommand) {
if (command.__t) { private __parseCommandLocales(command: Command | SubCommand | ContextMenuCommand) {
command.name_localizations = {}; if (command.__t) {
command.description_localizations = {}; command.name_localizations = {};
for (const locale of Object.keys(this.client.langs!.values)) { command.description_localizations = {};
const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? []; for (const locale of Object.keys(this.client.langs!.values)) {
if (Object.values<string>(Locale).includes(locale)) locales.push(locale as LocaleString); const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? [];
if (Object.values<string>(Locale).includes(locale)) locales.push(locale as LocaleString);
if (command.__t.name) {
for (const i of locales) { if (command.__t.name) {
const valueName = this.client.langs!.getKey(locale, command.__t.name!); for (const i of locales) {
if (valueName) command.name_localizations[i] = valueName; const valueName = this.client.langs!.getKey(locale, command.__t.name!);
} if (valueName) command.name_localizations[i] = valueName;
} }
}
if (command.__t.description) {
for (const i of locales) { if (command.__t.description) {
const valueKey = this.client.langs!.getKey(locale, command.__t.description!); for (const i of locales) {
if (valueKey) command.description_localizations[i] = valueKey; const valueKey = this.client.langs!.getKey(locale, command.__t.description!);
} if (valueKey) command.description_localizations[i] = valueKey;
} }
} }
} }
}
if (command instanceof ContextMenuCommand) return;
if (command instanceof ContextMenuCommand) return;
for (const options of command.options ?? []) {
if (options instanceof SubCommand || !options.locales) continue; for (const options of command.options ?? []) {
options.name_localizations = {}; if (options instanceof SubCommand || !options.locales) continue;
options.description_localizations = {}; options.name_localizations = {};
for (const locale of Object.keys(this.client.langs!.values)) { options.description_localizations = {};
const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? []; for (const locale of Object.keys(this.client.langs!.values)) {
if (Object.values<string>(Locale).includes(locale)) locales.push(locale as LocaleString); const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? [];
if (Object.values<string>(Locale).includes(locale)) locales.push(locale as LocaleString);
if (options.locales.name) {
for (const i of locales) { if (options.locales.name) {
const valueName = this.client.langs!.getKey(locale, options.locales.name!); for (const i of locales) {
if (valueName) options.name_localizations[i] = valueName; const valueName = this.client.langs!.getKey(locale, options.locales.name!);
} if (valueName) options.name_localizations[i] = valueName;
} }
}
if (options.locales.description) {
for (const i of locales) { if (options.locales.description) {
const valueKey = this.client.langs!.getKey(locale, options.locales.description!); for (const i of locales) {
if (valueKey) options.description_localizations[i] = valueKey; const valueKey = this.client.langs!.getKey(locale, options.locales.description!);
} if (valueKey) options.description_localizations[i] = valueKey;
} }
} }
} }
}
if (command instanceof Command && command.__tGroups) {
command.groups = {}; if (command instanceof Command && command.__tGroups) {
for (const locale of Object.keys(this.client.langs!.values)) { command.groups = {};
const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? []; for (const locale of Object.keys(this.client.langs!.values)) {
if (Object.values<string>(Locale).includes(locale)) locales.push(locale as LocaleString); const locales = this.client.langs!.aliases.find(x => x[0] === locale)?.[1] ?? [];
for (const group in command.__tGroups) { if (Object.values<string>(Locale).includes(locale)) locales.push(locale as LocaleString);
command.groups[group] ??= { for (const group in command.__tGroups) {
defaultDescription: command.__tGroups[group].defaultDescription, command.groups[group] ??= {
description: [], defaultDescription: command.__tGroups[group].defaultDescription,
name: [], description: [],
}; name: [],
};
if (command.__tGroups[group].name) {
for (const i of locales) { if (command.__tGroups[group].name) {
const valueName = this.client.langs!.getKey(locale, command.__tGroups[group].name!); for (const i of locales) {
if (valueName) { const valueName = this.client.langs!.getKey(locale, command.__tGroups[group].name!);
command.groups[group].name!.push([i, valueName]); if (valueName) {
} command.groups[group].name!.push([i, valueName]);
} }
} }
}
if (command.__tGroups[group].description) {
for (const i of locales) { if (command.__tGroups[group].description) {
const valueKey = this.client.langs!.getKey(locale, command.__tGroups[group].description!); for (const i of locales) {
if (valueKey) { const valueKey = this.client.langs!.getKey(locale, command.__tGroups[group].description!);
command.groups[group].description!.push([i, valueKey]); if (valueKey) {
} command.groups[group].description!.push([i, valueKey]);
} }
} }
} }
} }
} }
} }
}
setHandlers({
onCommand, setHandlers({
onSubCommand, onCommand,
}: { onSubCommand,
onCommand?: CommandHandler['onCommand']; }: {
onSubCommand?: CommandHandler['onSubCommand']; onCommand?: CommandHandler['onCommand'];
}) { onSubCommand?: CommandHandler['onSubCommand'];
if (onCommand) this.onCommand = onCommand; }) {
if (onSubCommand) this.onSubCommand = onSubCommand; if (onCommand) this.onCommand = onCommand;
} if (onSubCommand) this.onSubCommand = onSubCommand;
}
onCommand = (file: { new (): Command | SubCommand | ContextMenuCommand }):
| Command onCommand = (file: { new (): Command | SubCommand | ContextMenuCommand }):
| SubCommand | Command
| ContextMenuCommand | SubCommand
| false => new file(); | ContextMenuCommand
onSubCommand = (file: { new (): SubCommand }): SubCommand | false => new file(); | false => new file();
} onSubCommand = (file: { new (): SubCommand }): SubCommand | false => new file();
}