diff --git a/src/cache/resources/members.ts b/src/cache/resources/members.ts index d491b0c..0a5cb80 100644 --- a/src/cache/resources/members.ts +++ b/src/cache/resources/members.ts @@ -1,70 +1,70 @@ -import type { APIGuildMember } from 'discord-api-types/v10'; -import type { ReturnCache } from '../..'; -import { fakePromise } from '../../common'; -import { GuildMember } from '../../structures'; -import { GuildBasedResource } from './default/guild-based'; -export class Members extends GuildBasedResource { - namespace = 'member'; - - //@ts-expect-error - filter(data: APIGuildMember, id: string, guild_id: string) { - return true; - } - - override parse(data: any, key: string, guild_id: string) { - const { user, ...rest } = super.parse(data, data.user?.id ?? key, guild_id); - return rest; - } - - override get(id: string, guild: string): ReturnCache { - return fakePromise(super.get(id, guild)).then(rawMember => - fakePromise(this.client.cache.users?.get(id)).then(user => - rawMember && user ? new GuildMember(this.client, rawMember, user, guild) : undefined, - ), - ); - } - - override bulk(ids: string[], guild: string): ReturnCache { - return fakePromise(super.bulk(ids, guild)).then(members => - fakePromise(this.client.cache.users?.bulk(ids) ?? []).then( - users => - members - .map(rawMember => { - const user = users.find(x => x.id === rawMember.id); - return user ? new GuildMember(this.client, rawMember, user, guild) : undefined; - }) - .filter(Boolean) as GuildMember[], - ), - ); - } - - override values(guild: string): ReturnCache { - return fakePromise(super.values(guild)).then(members => - fakePromise(this.client.cache.users?.values() ?? []).then( - users => - members - .map(rawMember => { - const user = users.find(x => x.id === rawMember.id); - return user ? new GuildMember(this.client, rawMember, user, rawMember.guild_id) : undefined; - }) - .filter(Boolean) as GuildMember[], - ), - ); - } - - override async set(memberId: string, guildId: string, data: any): Promise; - override async set(memberId_dataArray: [string, any][], guildId: string): Promise; - override async set(__keys: string | [string, any][], guild: string, data?: any) { - const keys: [string, any][] = Array.isArray(__keys) ? __keys : [[__keys, data]]; - const bulkData: (['members', any, string, string] | ['users', any, string])[] = []; - - for (const [id, value] of keys) { - if (value.user) { - bulkData.push(['members', value, id, guild]); - bulkData.push(['users', value.user, id]); - } - } - - await this.cache.bulkSet(bulkData); - } -} +import type { APIGuildMember } from 'discord-api-types/v10'; +import type { ReturnCache } from '../..'; +import { fakePromise } from '../../common'; +import { GuildMember } from '../../structures'; +import { GuildBasedResource } from './default/guild-based'; +export class Members extends GuildBasedResource { + namespace = 'member'; + + //@ts-expect-error + filter(data: APIGuildMember, id: string, guild_id: string) { + return true; + } + + override parse(data: any, key: string, guild_id: string) { + const { user, ...rest } = super.parse(data, data.user?.id ?? key, guild_id); + return rest; + } + + override get(id: string, guild: string): ReturnCache { + return fakePromise(super.get(id, guild)).then(rawMember => + fakePromise(this.client.cache.users?.get(id)).then(user => + rawMember && user ? new GuildMember(this.client, rawMember, user, guild) : undefined, + ), + ); + } + + override bulk(ids: string[], guild: string): ReturnCache { + return fakePromise(super.bulk(ids, guild)).then(members => + fakePromise(this.client.cache.users?.bulk(ids)).then( + users => + members + .map(rawMember => { + const user = users?.find(x => x.id === rawMember.id); + return user ? new GuildMember(this.client, rawMember, user, guild) : undefined; + }) + .filter(Boolean) as GuildMember[], + ), + ); + } + + override values(guild: string): ReturnCache { + return fakePromise(super.values(guild)).then(members => + fakePromise(this.client.cache.users?.values()).then( + users => + members + .map(rawMember => { + const user = users?.find(x => x.id === rawMember.id); + return user ? new GuildMember(this.client, rawMember, user, rawMember.guild_id) : undefined; + }) + .filter(Boolean) as GuildMember[], + ), + ); + } + + override async set(memberId: string, guildId: string, data: any): Promise; + override async set(memberId_dataArray: [string, any][], guildId: string): Promise; + override async set(__keys: string | [string, any][], guild: string, data?: any) { + const keys: [string, any][] = Array.isArray(__keys) ? __keys : [[__keys, data]]; + const bulkData: (['members', any, string, string] | ['users', any, string])[] = []; + + for (const [id, value] of keys) { + if (value.user) { + bulkData.push(['members', value, id, guild]); + bulkData.push(['users', value.user, id]); + } + } + + await this.cache.bulkSet(bulkData); + } +} diff --git a/src/client/collectors.ts b/src/client/collectors.ts index 5b188ed..c526d13 100644 --- a/src/client/collectors.ts +++ b/src/client/collectors.ts @@ -1,99 +1,115 @@ -import { randomUUID } from 'node:crypto'; -import type { Awaitable, CamelCase, SnakeCase } from '../common'; -import type { ClientNameEvents, GatewayEvents } from '../events'; -import type { ClientEvents } from '../events/hooks'; - -type SnakeCaseClientNameEvents = Uppercase>; - -type RunData = { - options: { - event: T; - idle?: number; - timeout?: number; - onStop?: (reason: string) => unknown; - filter: (arg: Awaited>]>) => Awaitable; - run: (arg: Awaited>]>, stop: (reason?: string) => void) => unknown; - }; - idle?: NodeJS.Timeout; - timeout?: NodeJS.Timeout; - nonce: string; -}; - -export class Collectors { - readonly values = new Map[]>(); - - private generateRandomUUID(name: SnakeCaseClientNameEvents) { - const collectors = this.values.get(name); - if (!collectors) return '*'; - - let nonce = randomUUID(); - - while (collectors.find(x => x.nonce === nonce)) { - nonce = randomUUID(); - } - - return nonce; - } - - create(options: RunData['options']) { - if (!this.values.has(options.event)) { - this.values.set(options.event, []); - } - - const nonce = this.generateRandomUUID(options.event); - - this.values.get(options.event)!.push({ - options: { - ...options, - name: options.event, - } as RunData['options'], - idle: - options.idle && options.idle > 0 - ? setTimeout(() => { - return this.delete(options.event, nonce, 'idle'); - }, options.idle) - : undefined, - timeout: - options.timeout && options.timeout > 0 - ? setTimeout(() => { - return this.delete(options.event, nonce, 'timeout'); - }, options.timeout) - : undefined, - nonce, - }); - return options; - } - - private delete(name: SnakeCaseClientNameEvents, nonce: string, reason = 'unknown') { - const collectors = this.values.get(name); - - if (!collectors?.length) { - if (collectors) this.values.delete(name); - return; - } - - const index = collectors.findIndex(x => x.nonce === nonce); - if (index === -1) return; - const collector = collectors[index]; - clearTimeout(collector.idle); - clearTimeout(collector.timeout); - collectors.splice(index, 1); - return collector.options.onStop?.(reason); - } - - /**@internal */ - async run(name: T, data: Awaited>]>) { - const collectors = this.values.get(name); - if (!collectors) return; - - for (const i of collectors) { - if (await i.options.filter(data)) { - i.idle?.refresh(); - await i.options.run(data, (reason = 'unknown') => { - return this.delete(i.options.event, i.nonce, reason); - }); - break; - } - } - } -} +import { randomUUID } from 'node:crypto'; +import type { Awaitable, CamelCase, SnakeCase } from '../common'; +import type { ClientNameEvents, GatewayEvents } from '../events'; +import type { ClientEvents } from '../events/hooks'; +import { error } from 'node:console'; + +type SnakeCaseClientNameEvents = Uppercase>; + +type RunData = { + options: { + event: T; + idle?: number; + timeout?: number; + onStop?: (reason: string) => unknown; + onStopError?: (reason: string, error: unknown) => unknown; + filter: (arg: Awaited>]>) => Awaitable; + run: (arg: Awaited>]>, stop: (reason?: string) => void) => unknown; + onRunError?: ( + arg: Awaited>]>, + error: unknown, + stop: (reason?: string) => void, + ) => unknown; + }; + idle?: NodeJS.Timeout; + timeout?: NodeJS.Timeout; + nonce: string; +}; + +export class Collectors { + readonly values = new Map[]>(); + + private generateRandomUUID(name: SnakeCaseClientNameEvents) { + const collectors = this.values.get(name); + if (!collectors) return '*'; + + let nonce = randomUUID(); + + while (collectors.find(x => x.nonce === nonce)) { + nonce = randomUUID(); + } + + return nonce; + } + + create(options: RunData['options']) { + if (!this.values.has(options.event)) { + this.values.set(options.event, []); + } + + const nonce = this.generateRandomUUID(options.event); + + this.values.get(options.event)!.push({ + options: { + ...options, + name: options.event, + } as RunData['options'], + idle: + options.idle && options.idle > 0 + ? setTimeout(() => { + return this.delete(options.event, nonce, 'idle'); + }, options.idle) + : undefined, + timeout: + options.timeout && options.timeout > 0 + ? setTimeout(() => { + return this.delete(options.event, nonce, 'timeout'); + }, options.timeout) + : undefined, + nonce, + }); + return options; + } + + private async delete(name: SnakeCaseClientNameEvents, nonce: string, reason = 'unknown') { + const collectors = this.values.get(name); + + if (!collectors?.length) { + if (collectors) this.values.delete(name); + return; + } + + const index = collectors.findIndex(x => x.nonce === nonce); + if (index === -1) return; + const collector = collectors[index]; + clearTimeout(collector.idle); + clearTimeout(collector.timeout); + collectors.splice(index, 1); + try { + await collector.options.onStop?.(reason); + } catch (e) { + await collector.options.onStopError?.(reason, error); + } + } + + /**@internal */ + async run(name: T, data: Awaited>]>) { + const collectors = this.values.get(name); + if (!collectors) return; + + for (const i of collectors) { + if (await i.options.filter(data)) { + i.idle?.refresh(); + const stop = (reason = 'unknown') => { + return this.delete(i.options.event, i.nonce, reason); + }; + try { + await i.options.run(data, stop); + } catch (e) { + await i.options.onRunError?.(data, e, stop); + } + break; + } + } + } +} diff --git a/src/commands/applications/chat.ts b/src/commands/applications/chat.ts index 049f1cd..c877d51 100644 --- a/src/commands/applications/chat.ts +++ b/src/commands/applications/chat.ts @@ -1,362 +1,361 @@ -import { - ApplicationCommandOptionType, - ApplicationCommandType, - type ApplicationIntegrationType, - type InteractionContextType, - type APIApplicationCommandBasicOption, - type APIApplicationCommandOption, - type APIApplicationCommandSubcommandGroupOption, - type LocaleString, -} from 'discord-api-types/v10'; -import type { - ComponentContext, - MenuCommandContext, - ModalContext, - PermissionStrings, - SeyfertNumberOption, - SeyfertStringOption, -} from '../..'; -import type { Attachment } from '../../builders'; -import { magicImport, type FlatObjectKeys } from '../../common'; -import type { AllChannels, AutocompleteInteraction, GuildRole, InteractionGuildMember, User } from '../../structures'; -import type { Groups, RegisteredMiddlewares } from '../decorators'; -import type { OptionResolver } from '../optionresolver'; -import type { CommandContext } from './chatcontext'; -import type { - DefaultLocale, - 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: InteractionGuildMember | User; - 7: AllChannels; - 8: GuildRole; - 9: GuildRole | AllChannels | User; - 10: number; // number - 11: Attachment; -} - -type Wrap = N extends - | ApplicationCommandOptionType.Subcommand - | ApplicationCommandOptionType.SubcommandGroup - ? never - : { - required?: boolean; - value?( - data: { context: CommandContext; value: ReturnOptionsTypes[N] }, - ok: OKFunction, - fail: StopFunction, - ): void; - } & { - description: string; - description_localizations?: APIApplicationCommandBasicOption['description_localizations']; - name_localizations?: APIApplicationCommandBasicOption['name_localizations']; - locales?: { - name?: FlatObjectKeys; - description?: FlatObjectKeys; - }; - }; - -export type __TypeWrapper = Wrap; - -export type __TypesWrapper = { - [P in keyof typeof ApplicationCommandOptionType]: `${(typeof ApplicationCommandOptionType)[P]}` extends `${infer D extends - number}` - ? Wrap - : 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; - -type KeysWithoutRequired = { - [K in keyof T]-?: T[K]['required'] extends true ? never : K; -}[keyof T]; - -type ContextOptionsAux = { - [K in Exclude>]: T[K]['value'] extends (...args: any) => any - ? Parameters[1]>[0] - : T[K] extends SeyfertStringOption | SeyfertNumberOption - ? T[K]['choices'] extends NonNullable - ? T[K]['choices'][number]['value'] - : ReturnOptionsTypes[T[K]['type']] - : ReturnOptionsTypes[T[K]['type']]; -} & { - [K in KeysWithoutRequired]?: T[K]['value'] extends (...args: any) => any - ? Parameters[1]>[0] - : T[K] extends SeyfertStringOption | SeyfertNumberOption - ? T[K]['choices'] extends NonNullable - ? T[K]['choices'][number]['value'] - : ReturnOptionsTypes[T[K]['type']] - : ReturnOptionsTypes[T[K]['type']]; -}; - -export type ContextOptions = ContextOptionsAux; - -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>; - description_localizations?: Partial>; - - options?: CommandOption[] | SubCommand[]; - - ignore?: IgnoreCommand; - - aliases?: string[]; - - /** @internal */ - async __runOptions( - ctx: CommandContext<{}, never>, - resolver: OptionResolver, - ): 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( - (res, rej) => - option.value?.({ context: ctx, value: resolver.getValue(i.name) } as never, res, rej) || - res(resolver.getValue(i.name)), - ) - : undefined; - if (value === undefined) { - if (option.required) { - errored = true; - data[i.name] = { - failed: true, - value: `${i.name} is required but returned no value`, - }; - 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 : `${e}`, - }; - } - } - return [errored, data]; - } - - /** @internal */ - static __runMiddlewares( - context: CommandContext<{}, never> | ComponentContext | MenuCommandContext | ModalContext, - 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(obj: any) { - if (!running) { - return; - } - // biome-ignore lint/style/noArguments: yes - // biome-ignore lint/correctness/noUndeclaredVariables: xd - if (arguments.length) { - // @ts-expect-error - context[global ? 'globalMetadata' : 'metadata'][middlewares[index]] = obj; - } - 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[0]; - groupsAliases?: Record; - __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; -} +import { + ApplicationCommandOptionType, + ApplicationCommandType, + type ApplicationIntegrationType, + type InteractionContextType, + type APIApplicationCommandBasicOption, + type APIApplicationCommandOption, + type APIApplicationCommandSubcommandGroupOption, + type LocaleString, +} from 'discord-api-types/v10'; +import type { + ComponentContext, + MenuCommandContext, + ModalContext, + PermissionStrings, + SeyfertNumberOption, + SeyfertStringOption, +} from '../..'; +import type { Attachment } from '../../builders'; +import { magicImport, type FlatObjectKeys } from '../../common'; +import type { AllChannels, AutocompleteInteraction, GuildRole, InteractionGuildMember, User } from '../../structures'; +import type { Groups, RegisteredMiddlewares } from '../decorators'; +import type { OptionResolver } from '../optionresolver'; +import type { CommandContext } from './chatcontext'; +import type { + DefaultLocale, + 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: InteractionGuildMember | User; + 7: AllChannels; + 8: GuildRole; + 9: GuildRole | AllChannels | User; + 10: number; // number + 11: Attachment; +} + +type Wrap = N extends + | ApplicationCommandOptionType.Subcommand + | ApplicationCommandOptionType.SubcommandGroup + ? never + : { + required?: boolean; + value?( + data: { context: CommandContext; value: ReturnOptionsTypes[N] }, + ok: OKFunction, + fail: StopFunction, + ): void; + } & { + description: string; + description_localizations?: APIApplicationCommandBasicOption['description_localizations']; + name_localizations?: APIApplicationCommandBasicOption['name_localizations']; + locales?: { + name?: FlatObjectKeys; + description?: FlatObjectKeys; + }; + }; + +export type __TypeWrapper = Wrap; + +export type __TypesWrapper = { + [P in keyof typeof ApplicationCommandOptionType]: `${(typeof ApplicationCommandOptionType)[P]}` extends `${infer D extends + number}` + ? Wrap + : 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; + +type KeysWithoutRequired = { + [K in keyof T]-?: T[K]['required'] extends true ? never : K; +}[keyof T]; + +type ContextOptionsAux = { + [K in Exclude>]: T[K]['value'] extends (...args: any) => any + ? Parameters[1]>[0] + : T[K] extends SeyfertStringOption | SeyfertNumberOption + ? T[K]['choices'] extends NonNullable + ? T[K]['choices'][number]['value'] + : ReturnOptionsTypes[T[K]['type']] + : ReturnOptionsTypes[T[K]['type']]; +} & { + [K in KeysWithoutRequired]?: T[K]['value'] extends (...args: any) => any + ? Parameters[1]>[0] + : T[K] extends SeyfertStringOption | SeyfertNumberOption + ? T[K]['choices'] extends NonNullable + ? T[K]['choices'][number]['value'] + : ReturnOptionsTypes[T[K]['type']] + : ReturnOptionsTypes[T[K]['type']]; +}; + +export type ContextOptions = ContextOptionsAux; + +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>; + description_localizations?: Partial>; + + options?: CommandOption[] | SubCommand[]; + + ignore?: IgnoreCommand; + + aliases?: string[]; + + /** @internal */ + async __runOptions( + ctx: CommandContext<{}, never>, + resolver: OptionResolver, + ): 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( + (res, rej) => + option.value?.({ context: ctx, value: resolver.getValue(i.name) } as never, res, rej) || + res(resolver.getValue(i.name)), + ) + : undefined; + if (value === undefined) { + if (option.required) { + errored = true; + data[i.name] = { + failed: true, + value: `${i.name} is required but returned no value`, + }; + 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 : `${e}`, + }; + } + } + return [errored, data]; + } + + /** @internal */ + static __runMiddlewares( + context: CommandContext<{}, never> | ComponentContext | MenuCommandContext | ModalContext, + 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(obj: any) { + if (!running) { + return; + } + // biome-ignore lint/style/noArguments: yes + // biome-ignore lint/correctness/noUndeclaredVariables: xd + if (arguments.length) { + // @ts-expect-error + context[global ? 'globalMetadata' : 'metadata'][middlewares[index]] = obj; + } + 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[0]; + groupsAliases?: Record; + __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; +} diff --git a/src/events/handler.ts b/src/events/handler.ts index 06141ba..fe10896 100644 --- a/src/events/handler.ts +++ b/src/events/handler.ts @@ -73,8 +73,10 @@ export class EventHandler extends BaseHandler { break; } - await this.runEvent(args[0].t, args[1], args[0].d, args[2]); - await this.client.collectors.run(args[0].t, args[0].d); + await Promise.all([ + this.runEvent(args[0].t, args[1], args[0].d, args[2]), + this.client.collectors.run(args[0].t, args[0].d), + ]); } async runEvent(name: GatewayEvents, client: Client | WorkerClient, packet: any, shardId: number) { diff --git a/src/structures/Message.ts b/src/structures/Message.ts index d33aab1..baabe22 100644 --- a/src/structures/Message.ts +++ b/src/structures/Message.ts @@ -1,271 +1,274 @@ -import type { - APIChannelMention, - APIEmbed, - APIGuildMember, - APIMessage, - APIUser, - GatewayMessageCreateDispatchData, -} from 'discord-api-types/v10'; -import type { ListenerOptions } from '../builders'; -import type { UsingClient } from '../commands'; -import { toCamelCase, type ObjectToLower } from '../common'; -import type { EmojiResolvable } from '../common/types/resolvables'; -import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../common/types/write'; -import type { ActionRowMessageComponents } from '../components'; -import { MessageActionRowComponent } from '../components/ActionRow'; -import { GuildMember } from './GuildMember'; -import { User } from './User'; -import type { MessageWebhookMethodEditParams, MessageWebhookMethodWriteParams } from './Webhook'; -import { DiscordBase } from './extra/DiscordBase'; -import { messageLink } from './extra/functions'; -import { Embed, Poll } from '..'; - -export type MessageData = APIMessage | GatewayMessageCreateDispatchData; - -export interface BaseMessage - extends DiscordBase, - ObjectToLower> {} -export class BaseMessage extends DiscordBase { - guildId: string | undefined; - timestamp?: number; - author!: User; - member?: GuildMember; - components: MessageActionRowComponent[]; - poll?: Poll; - mentions: { - roles: string[]; - channels: APIChannelMention[]; - users: (GuildMember | User)[]; - }; - embeds: InMessageEmbed[]; - - constructor(client: UsingClient, data: MessageData) { - super(client, data); - this.mentions = { - roles: data.mention_roles ?? [], - channels: data.mention_channels ?? [], - users: [], - }; - this.components = (data.components ?? []).map(x => new MessageActionRowComponent(x)); - this.embeds = data.embeds.map(embed => new InMessageEmbed(embed)); - this.patch(data); - } - - get user() { - return this.author; - } - - createComponentCollector(options?: ListenerOptions) { - return this.client.components!.createComponentCollector(this.id, options); - } - - get url() { - return messageLink(this.channelId, this.id, this.guildId); - } - - guild(force = false) { - if (!this.guildId) return; - return this.client.guilds.fetch(this.guildId, force); - } - - async channel(force = false) { - return this.client.channels.fetch(this.channelId, force); - } - - react(emoji: EmojiResolvable) { - return this.client.reactions.add(this.id, this.channelId, emoji); - } - - private patch(data: MessageData) { - if ('guild_id' in data) { - this.guildId = data.guild_id; - } - - if (data.type !== undefined) { - this.type = data.type; - } - - if ('timestamp' in data && data.timestamp) { - this.timestamp = Date.parse(data.timestamp); - } - - if ('application_id' in data) { - this.applicationId = data.application_id; - } - if ('author' in data && data.author) { - this.author = new User(this.client, data.author); - } - - if ('member' in data && data.member) { - this.member = new GuildMember(this.client, data.member, this.author, this.guildId!); - } - - if (data.mentions?.length) { - this.mentions.users = this.guildId - ? data.mentions.map( - m => - new GuildMember( - this.client, - { - ...(m as APIUser & { member?: Omit }).member!, - user: m, - }, - m, - this.guildId!, - ), - ) - : data.mentions.map(u => new User(this.client, u)); - } - - if (data.poll) { - this.poll = new Poll(this.client, data.poll, this.channelId, this.id); - } - } -} - -export interface Message - extends BaseMessage, - ObjectToLower> {} - -export class Message extends BaseMessage { - constructor(client: UsingClient, data: MessageData) { - super(client, data); - } - - fetch() { - return this.client.messages.fetch(this.id, this.channelId); - } - - reply(body: Omit, fail = true) { - return this.write({ - ...body, - message_reference: { - message_id: this.id, - channel_id: this.channelId, - guild_id: this.guildId, - fail_if_not_exists: fail, - }, - }); - } - - edit(body: MessageUpdateBodyRequest) { - return this.client.messages.edit(this.id, this.channelId, body); - } - - write(body: MessageCreateBodyRequest) { - return this.client.messages.write(this.channelId, body); - } - - delete(reason?: string) { - return this.client.messages.delete(this.id, this.channelId, reason); - } - - crosspost(reason?: string) { - return this.client.messages.crosspost(this.id, this.channelId, reason); - } -} - -export type EditMessageWebhook = Omit['body'] & - Pick; -export type WriteMessageWebhook = MessageWebhookMethodWriteParams['body'] & - Pick; - -export class WebhookMessage extends BaseMessage { - constructor( - client: UsingClient, - data: MessageData, - readonly webhookId: string, - readonly webhookToken: string, - ) { - super(client, data); - } - - fetch() { - return this.api.webhooks(this.webhookId)(this.webhookToken).get({ query: this.thread?.id }); - } - - edit(body: EditMessageWebhook) { - const { query, ...rest } = body; - return this.client.webhooks.editMessage(this.webhookId, this.webhookToken, { - body: rest, - query, - messageId: this.id, - }); - } - - write(body: WriteMessageWebhook) { - const { query, ...rest } = body; - return this.client.webhooks.writeMessage(this.webhookId, this.webhookToken, { - body: rest, - query, - }); - } - - delete(reason?: string) { - return this.client.webhooks.deleteMessage(this.webhookId, this.webhookToken, this.id, reason); - } -} - -export class InMessageEmbed { - constructor(public data: APIEmbed) {} - - get title() { - return this.data.title; - } - - get type() { - return this.data.type; - } - - get description() { - return this.data.description; - } - - get url() { - return this.data.url; - } - - get timestamp() { - return this.data.timestamp; - } - - get color() { - return this.data.color; - } - - get footer() { - return toCamelCase(this.data.footer ?? {}); - } - - get image() { - return toCamelCase(this.data.image ?? {}); - } - - get thumbnail() { - return toCamelCase(this.data.thumbnail ?? {}); - } - - get video() { - return toCamelCase(this.data.video ?? {}); - } - - get provider() { - return this.data.provider; - } - - get author() { - return toCamelCase(this.data.author ?? {}); - } - - get fields() { - return this.data.fields; - } - - toBuilder() { - return new Embed(this.data); - } - - toJSON() { - return { ...this.data }; - } -} +import type { + APIChannelMention, + APIEmbed, + APIGuildMember, + APIMessage, + APIUser, + GatewayMessageCreateDispatchData, +} from 'discord-api-types/v10'; +import type { ListenerOptions } from '../builders'; +import type { UsingClient } from '../commands'; +import { toCamelCase, type ObjectToLower } from '../common'; +import type { EmojiResolvable } from '../common/types/resolvables'; +import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../common/types/write'; +import type { ActionRowMessageComponents } from '../components'; +import { MessageActionRowComponent } from '../components/ActionRow'; +import { GuildMember } from './GuildMember'; +import { User } from './User'; +import type { MessageWebhookMethodEditParams, MessageWebhookMethodWriteParams } from './Webhook'; +import { DiscordBase } from './extra/DiscordBase'; +import { messageLink } from './extra/functions'; +import { Embed, Poll } from '..'; + +export type MessageData = APIMessage | GatewayMessageCreateDispatchData; + +export interface BaseMessage + extends DiscordBase, + ObjectToLower> {} +export class BaseMessage extends DiscordBase { + guildId: string | undefined; + timestamp?: number; + author!: User; + member?: GuildMember; + components: MessageActionRowComponent[]; + poll?: Poll; + mentions: { + roles: string[]; + channels: APIChannelMention[]; + users: (GuildMember | User)[]; + }; + embeds: InMessageEmbed[]; + + constructor(client: UsingClient, data: MessageData) { + super(client, data); + this.mentions = { + roles: data.mention_roles ?? [], + channels: data.mention_channels ?? [], + users: [], + }; + this.components = data.components?.map(x => new MessageActionRowComponent(x)) ?? []; + this.embeds = data.embeds.map(embed => new InMessageEmbed(embed)); + this.patch(data); + } + + get user() { + return this.author; + } + + createComponentCollector(options?: ListenerOptions) { + return this.client.components!.createComponentCollector(this.id, options); + } + + get url() { + return messageLink(this.channelId, this.id, this.guildId); + } + + guild(force = false) { + if (!this.guildId) return; + return this.client.guilds.fetch(this.guildId, force); + } + + async channel(force = false) { + return this.client.channels.fetch(this.channelId, force); + } + + react(emoji: EmojiResolvable) { + return this.client.reactions.add(this.id, this.channelId, emoji); + } + + private patch(data: MessageData) { + if ('guild_id' in data) { + this.guildId = data.guild_id; + } + + if (data.type !== undefined) { + this.type = data.type; + } + + if ('timestamp' in data && data.timestamp) { + this.timestamp = Date.parse(data.timestamp); + } + + if ('application_id' in data) { + this.applicationId = data.application_id; + } + if ('author' in data && data.author) { + this.author = new User(this.client, data.author); + } + + if ('member' in data && data.member) { + this.member = new GuildMember(this.client, data.member, this.author, this.guildId!); + } + + if (data.mentions?.length) { + this.mentions.users = this.guildId + ? data.mentions.map( + m => + new GuildMember( + this.client, + { + ...(m as APIUser & { member?: Omit }).member!, + user: m, + }, + m, + this.guildId!, + ), + ) + : data.mentions.map(u => new User(this.client, u)); + } + + if (data.poll) { + this.poll = new Poll(this.client, data.poll, this.channelId, this.id); + } + } +} + +export interface Message + extends BaseMessage, + ObjectToLower> {} + +export class Message extends BaseMessage { + constructor(client: UsingClient, data: MessageData) { + super(client, data); + } + + fetch() { + return this.client.messages.fetch(this.id, this.channelId); + } + + reply(body: Omit, fail = true) { + return this.write({ + ...body, + message_reference: { + message_id: this.id, + channel_id: this.channelId, + guild_id: this.guildId, + fail_if_not_exists: fail, + }, + }); + } + + edit(body: MessageUpdateBodyRequest) { + return this.client.messages.edit(this.id, this.channelId, body); + } + + write(body: MessageCreateBodyRequest) { + return this.client.messages.write(this.channelId, body); + } + + delete(reason?: string) { + return this.client.messages.delete(this.id, this.channelId, reason); + } + + crosspost(reason?: string) { + return this.client.messages.crosspost(this.id, this.channelId, reason); + } +} + +export type EditMessageWebhook = Omit['body'] & + Pick; +export type WriteMessageWebhook = MessageWebhookMethodWriteParams['body'] & + Pick; + +export class WebhookMessage extends BaseMessage { + constructor( + client: UsingClient, + data: MessageData, + readonly webhookId: string, + readonly webhookToken: string, + ) { + super(client, data); + } + + fetch() { + return this.api.webhooks(this.webhookId)(this.webhookToken).get({ query: this.thread?.id }); + } + + edit(body: EditMessageWebhook) { + const { query, ...rest } = body; + return this.client.webhooks.editMessage(this.webhookId, this.webhookToken, { + body: rest, + query, + messageId: this.id, + }); + } + + write(body: WriteMessageWebhook) { + const { query, ...rest } = body; + return this.client.webhooks.writeMessage(this.webhookId, this.webhookToken, { + body: rest, + query, + }); + } + + delete(reason?: string) { + return this.client.webhooks.deleteMessage(this.webhookId, this.webhookToken, this.id, reason); + } +} + +export class InMessageEmbed { + constructor(public data: APIEmbed) {} + + get title() { + return this.data.title; + } + + /** + * @deprecated + */ + get type() { + return this.data.type; + } + + get description() { + return this.data.description; + } + + get url() { + return this.data.url; + } + + get timestamp() { + return this.data.timestamp; + } + + get color() { + return this.data.color; + } + + get footer() { + return this.data.footer ? toCamelCase(this.data.footer) : undefined; + } + + get image() { + return this.data.image ? toCamelCase(this.data.image) : undefined; + } + + get thumbnail() { + return this.data.thumbnail ? toCamelCase(this.data.thumbnail) : undefined; + } + + get video() { + return this.data.video ? toCamelCase(this.data.video) : undefined; + } + + get provider() { + return this.data.provider; + } + + get author() { + return this.data.author ? toCamelCase(this.data.author) : undefined; + } + + get fields() { + return this.data.fields; + } + + toBuilder() { + return new Embed(this.data); + } + + toJSON() { + return { ...this.data }; + } +}