mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-01 20:46:08 +00:00
374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
import { inspect } from 'node:util';
|
|
import type {
|
|
ComponentContext,
|
|
EntryPointContext,
|
|
MenuCommandContext,
|
|
ModalContext,
|
|
PermissionStrings,
|
|
SeyfertNumberOption,
|
|
SeyfertStringOption,
|
|
} from '../..';
|
|
import type { Attachment } from '../../builders';
|
|
import type {
|
|
GuildRoleStructure,
|
|
InteractionGuildMemberStructure,
|
|
OptionResolverStructure,
|
|
UserStructure,
|
|
} from '../../client/transformers';
|
|
import { type Awaitable, type FlatObjectKeys, magicImport } from '../../common';
|
|
import type { AllChannels, AutocompleteInteraction } from '../../structures';
|
|
import {
|
|
type APIApplicationCommandBasicOption,
|
|
type APIApplicationCommandOption,
|
|
type APIApplicationCommandSubcommandGroupOption,
|
|
ApplicationCommandOptionType,
|
|
ApplicationCommandType,
|
|
type ApplicationIntegrationType,
|
|
type InteractionContextType,
|
|
type LocaleString,
|
|
} from '../../types';
|
|
import type { Groups, RegisteredMiddlewares } from '../decorators';
|
|
import type { CommandContext } from './chatcontext';
|
|
import type {
|
|
DefaultLocale,
|
|
ExtraProps,
|
|
IgnoreCommand,
|
|
OKFunction,
|
|
OnOptionsReturnObject,
|
|
PassFunction,
|
|
StopFunction,
|
|
UsingClient,
|
|
} from './shared';
|
|
|
|
export interface ReturnOptionsTypes {
|
|
1: never; // subcommand
|
|
2: never; // subcommandgroup
|
|
3: string;
|
|
4: number; // integer
|
|
5: boolean;
|
|
6: InteractionGuildMemberStructure | UserStructure;
|
|
7: AllChannels;
|
|
8: GuildRoleStructure;
|
|
9: GuildRoleStructure | AllChannels | UserStructure;
|
|
10: number; // number
|
|
11: Attachment;
|
|
}
|
|
|
|
type Wrap<N extends ApplicationCommandOptionType> = N extends
|
|
| ApplicationCommandOptionType.Subcommand
|
|
| ApplicationCommandOptionType.SubcommandGroup
|
|
? never
|
|
: {
|
|
required?: boolean;
|
|
value?(
|
|
data: { context: CommandContext; value: ReturnOptionsTypes[N] },
|
|
ok: OKFunction<any>,
|
|
fail: StopFunction,
|
|
): Awaitable<void>;
|
|
} & {
|
|
description: string;
|
|
description_localizations?: APIApplicationCommandBasicOption['description_localizations'];
|
|
name_localizations?: APIApplicationCommandBasicOption['name_localizations'];
|
|
locales?: {
|
|
name?: FlatObjectKeys<DefaultLocale>;
|
|
description?: FlatObjectKeys<DefaultLocale>;
|
|
};
|
|
};
|
|
|
|
export type __TypeWrapper<T extends ApplicationCommandOptionType> = Wrap<T>;
|
|
|
|
export type __TypesWrapper = {
|
|
[P in keyof typeof ApplicationCommandOptionType]: `${(typeof ApplicationCommandOptionType)[P]}` extends `${infer D extends
|
|
number}`
|
|
? Wrap<D>
|
|
: never;
|
|
};
|
|
|
|
export type AutocompleteCallback = (interaction: AutocompleteInteraction) => any;
|
|
export type OnAutocompleteErrorCallback = (interaction: AutocompleteInteraction, error: unknown) => any;
|
|
export type CommandBaseOption = __TypesWrapper[keyof __TypesWrapper];
|
|
export type CommandBaseAutocompleteOption = __TypesWrapper[keyof __TypesWrapper] & {
|
|
autocomplete: AutocompleteCallback;
|
|
onAutocompleteError?: OnAutocompleteErrorCallback;
|
|
};
|
|
export type CommandAutocompleteOption = CommandBaseAutocompleteOption & { name: string };
|
|
export type __CommandOption = CommandBaseOption; //| CommandBaseAutocompleteOption;
|
|
export type CommandOption = __CommandOption & { name: string };
|
|
export type OptionsRecord = Record<string, __CommandOption & { type: ApplicationCommandOptionType }>;
|
|
|
|
type KeysWithoutRequired<T extends OptionsRecord> = {
|
|
[K in keyof T]-?: T[K]['required'] extends true ? never : K;
|
|
}[keyof T];
|
|
|
|
type ContextOptionsAux<T extends OptionsRecord> = {
|
|
[K in Exclude<keyof T, KeysWithoutRequired<T>>]: T[K]['value'] extends (...args: any) => any
|
|
? Parameters<Parameters<T[K]['value']>[1]>[0]
|
|
: T[K] extends SeyfertStringOption | SeyfertNumberOption
|
|
? T[K]['choices'] extends NonNullable<SeyfertStringOption['choices'] | SeyfertNumberOption['choices']>
|
|
? T[K]['choices'][number]['value']
|
|
: ReturnOptionsTypes[T[K]['type']]
|
|
: ReturnOptionsTypes[T[K]['type']];
|
|
} & {
|
|
[K in KeysWithoutRequired<T>]?: T[K]['value'] extends (...args: any) => any
|
|
? Parameters<Parameters<T[K]['value']>[1]>[0]
|
|
: T[K] extends SeyfertStringOption | SeyfertNumberOption
|
|
? T[K]['choices'] extends NonNullable<SeyfertStringOption['choices'] | SeyfertNumberOption['choices']>
|
|
? T[K]['choices'][number]['value']
|
|
: ReturnOptionsTypes[T[K]['type']]
|
|
: ReturnOptionsTypes[T[K]['type']];
|
|
};
|
|
|
|
export type ContextOptions<T extends OptionsRecord> = ContextOptionsAux<T>;
|
|
|
|
export class BaseCommand {
|
|
middlewares: (keyof RegisteredMiddlewares)[] = [];
|
|
|
|
__filePath?: string;
|
|
__t?: { name: string | undefined; description: string | undefined };
|
|
__autoload?: true;
|
|
|
|
guildId?: string[];
|
|
name!: string;
|
|
type!: number; // ApplicationCommandType.ChatInput | ApplicationCommandOptionType.Subcommand
|
|
nsfw?: boolean;
|
|
description!: string;
|
|
defaultMemberPermissions?: bigint;
|
|
integrationTypes: ApplicationIntegrationType[] = [];
|
|
contexts: InteractionContextType[] = [];
|
|
botPermissions?: bigint;
|
|
name_localizations?: Partial<Record<LocaleString, string>>;
|
|
description_localizations?: Partial<Record<LocaleString, string>>;
|
|
|
|
options?: CommandOption[] | SubCommand[];
|
|
|
|
ignore?: IgnoreCommand;
|
|
|
|
aliases?: string[];
|
|
|
|
props: ExtraProps = {};
|
|
|
|
/** @internal */
|
|
async __runOptions(
|
|
ctx: CommandContext<{}, never>,
|
|
resolver: OptionResolverStructure,
|
|
): Promise<[boolean, OnOptionsReturnObject]> {
|
|
if (!this?.options?.length) {
|
|
return [false, {}];
|
|
}
|
|
const data: OnOptionsReturnObject = {};
|
|
let errored = false;
|
|
for (const i of this.options ?? []) {
|
|
try {
|
|
const option = this.options!.find(x => x.name === i.name) as __CommandOption;
|
|
const value =
|
|
resolver.getHoisted(i.name)?.value !== undefined
|
|
? await new Promise(async (res, rej) => {
|
|
try {
|
|
(await option.value?.({ context: ctx, value: resolver.getValue(i.name) } as never, res, rej)) ||
|
|
res(resolver.getValue(i.name));
|
|
} catch (e) {
|
|
rej(e);
|
|
}
|
|
})
|
|
: undefined;
|
|
if (value === undefined) {
|
|
if (option.required) {
|
|
errored = true;
|
|
data[i.name] = {
|
|
failed: true,
|
|
value: `${i.name} is required but returned no value`,
|
|
parseError: undefined,
|
|
};
|
|
continue;
|
|
}
|
|
}
|
|
// @ts-expect-error
|
|
ctx.options[i.name] = value;
|
|
data[i.name] = {
|
|
failed: false,
|
|
value,
|
|
};
|
|
} catch (e) {
|
|
errored = true;
|
|
data[i.name] = {
|
|
failed: true,
|
|
value: e instanceof Error ? e.message : typeof e === 'string' ? e : inspect(e),
|
|
parseError: undefined,
|
|
};
|
|
}
|
|
}
|
|
return [errored, data];
|
|
}
|
|
|
|
/** @internal */
|
|
static __runMiddlewares(
|
|
context: CommandContext<{}, never> | ComponentContext | MenuCommandContext<any> | ModalContext | EntryPointContext,
|
|
middlewares: (keyof RegisteredMiddlewares)[],
|
|
global: boolean,
|
|
): Promise<{ error?: string; pass?: boolean }> {
|
|
if (!middlewares.length) {
|
|
return Promise.resolve({});
|
|
}
|
|
let index = 0;
|
|
|
|
return new Promise(res => {
|
|
let running = true;
|
|
const pass: PassFunction = () => {
|
|
if (!running) {
|
|
return;
|
|
}
|
|
running = false;
|
|
return res({ pass: true });
|
|
};
|
|
function next(...args: unknown[]) {
|
|
if (!running) {
|
|
return;
|
|
}
|
|
if (args.length) {
|
|
context[global ? 'globalMetadata' : 'metadata'][middlewares[index]] = args[0] as never;
|
|
}
|
|
if (++index >= middlewares.length) {
|
|
running = false;
|
|
return res({});
|
|
}
|
|
context.client.middlewares![middlewares[index]]({ context, next, stop, pass });
|
|
}
|
|
const stop: StopFunction = err => {
|
|
if (!running) {
|
|
return;
|
|
}
|
|
running = false;
|
|
return res({ error: err });
|
|
};
|
|
context.client.middlewares![middlewares[0]]({ context, next, stop, pass });
|
|
});
|
|
}
|
|
|
|
/** @internal */
|
|
__runMiddlewares(context: CommandContext<{}, never>) {
|
|
return BaseCommand.__runMiddlewares(context, this.middlewares as (keyof RegisteredMiddlewares)[], false);
|
|
}
|
|
|
|
/** @internal */
|
|
__runGlobalMiddlewares(context: CommandContext<{}, never>) {
|
|
return BaseCommand.__runMiddlewares(
|
|
context,
|
|
(context.client.options?.globalMiddlewares ?? []) as (keyof RegisteredMiddlewares)[],
|
|
true,
|
|
);
|
|
}
|
|
|
|
toJSON() {
|
|
const data = {
|
|
name: this.name,
|
|
type: this.type,
|
|
nsfw: !!this.nsfw,
|
|
description: this.description,
|
|
name_localizations: this.name_localizations,
|
|
description_localizations: this.description_localizations,
|
|
guild_id: this.guildId,
|
|
default_member_permissions: this.defaultMemberPermissions ? this.defaultMemberPermissions.toString() : undefined,
|
|
contexts: this.contexts,
|
|
integration_types: this.integrationTypes,
|
|
} as {
|
|
name: BaseCommand['name'];
|
|
type: BaseCommand['type'];
|
|
nsfw: BaseCommand['nsfw'];
|
|
description: BaseCommand['description'];
|
|
name_localizations: BaseCommand['name_localizations'];
|
|
description_localizations: BaseCommand['description_localizations'];
|
|
guild_id: BaseCommand['guildId'];
|
|
default_member_permissions: string;
|
|
contexts: BaseCommand['contexts'];
|
|
integration_types: BaseCommand['integrationTypes'];
|
|
};
|
|
return data;
|
|
}
|
|
|
|
async reload() {
|
|
delete require.cache[this.__filePath!];
|
|
|
|
for (const i of this.options ?? []) {
|
|
if (i instanceof SubCommand && i.__filePath) {
|
|
await i.reload();
|
|
}
|
|
}
|
|
|
|
const __tempCommand = await magicImport(this.__filePath!).then(x => x.default ?? x);
|
|
|
|
Object.setPrototypeOf(this, __tempCommand.prototype);
|
|
}
|
|
|
|
run?(context: CommandContext): any;
|
|
onAfterRun?(context: CommandContext, error: unknown | undefined): any;
|
|
onRunError?(context: CommandContext, error: unknown): any;
|
|
onOptionsError?(context: CommandContext, metadata: OnOptionsReturnObject): any;
|
|
onMiddlewaresError?(context: CommandContext, error: string): any;
|
|
onBotPermissionsFail?(context: CommandContext, permissions: PermissionStrings): any;
|
|
onPermissionsFail?(context: CommandContext, permissions: PermissionStrings): any;
|
|
onInternalError?(client: UsingClient, command: Command | SubCommand, error?: unknown): any;
|
|
}
|
|
|
|
export class Command extends BaseCommand {
|
|
type = ApplicationCommandType.ChatInput;
|
|
|
|
groups?: Parameters<typeof Groups>[0];
|
|
groupsAliases?: Record<string, string>;
|
|
__tGroups?: Record<
|
|
string /* name for group*/,
|
|
{
|
|
name: string | undefined;
|
|
description: string | undefined;
|
|
defaultDescription: string;
|
|
}
|
|
>;
|
|
|
|
toJSON() {
|
|
const options: APIApplicationCommandOption[] = [];
|
|
|
|
for (const i of this.options ?? []) {
|
|
if (!(i instanceof SubCommand)) {
|
|
options.push({ ...i, autocomplete: 'autocomplete' in i } as APIApplicationCommandBasicOption);
|
|
continue;
|
|
}
|
|
if (i.group) {
|
|
if (!options.find(x => x.name === i.group)) {
|
|
options.push({
|
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
|
name: i.group,
|
|
description: this.groups![i.group].defaultDescription,
|
|
description_localizations: Object.fromEntries(this.groups?.[i.group].description ?? []),
|
|
name_localizations: Object.fromEntries(this.groups?.[i.group].name ?? []),
|
|
options: [],
|
|
});
|
|
}
|
|
const group = options.find(x => x.name === i.group) as APIApplicationCommandSubcommandGroupOption;
|
|
group.options?.push(i.toJSON());
|
|
continue;
|
|
}
|
|
options.push(i.toJSON());
|
|
}
|
|
|
|
return {
|
|
...super.toJSON(),
|
|
options,
|
|
};
|
|
}
|
|
}
|
|
|
|
export abstract class SubCommand extends BaseCommand {
|
|
type = ApplicationCommandOptionType.Subcommand;
|
|
group?: string;
|
|
declare options?: CommandOption[];
|
|
|
|
toJSON() {
|
|
return {
|
|
...super.toJSON(),
|
|
options:
|
|
this.options?.map(x => ({ ...x, autocomplete: 'autocomplete' in x }) as APIApplicationCommandBasicOption) ?? [],
|
|
};
|
|
}
|
|
|
|
abstract run(context: CommandContext<any>): any;
|
|
}
|