feat: optimize code & implement onRunError and onStopError in collectors & use Promise.all

This commit is contained in:
MARCROCK22 2024-06-02 14:33:44 +00:00
parent 3850ef38a6
commit d2157e7324
5 changed files with 824 additions and 804 deletions

View File

@ -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<GuildMember | undefined> {
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<GuildMember[]> {
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<GuildMember[]> {
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<void>;
override async set(memberId_dataArray: [string, any][], guildId: string): Promise<void>;
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<GuildMember | undefined> {
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<GuildMember[]> {
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<GuildMember[]> {
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<void>;
override async set(memberId_dataArray: [string, any][], guildId: string): Promise<void>;
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);
}
}

View File

@ -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<SnakeCase<ClientNameEvents>>;
type RunData<T extends SnakeCaseClientNameEvents> = {
options: {
event: T;
idle?: number;
timeout?: number;
onStop?: (reason: string) => unknown;
filter: (arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>) => Awaitable<boolean>;
run: (arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>, stop: (reason?: string) => void) => unknown;
};
idle?: NodeJS.Timeout;
timeout?: NodeJS.Timeout;
nonce: string;
};
export class Collectors {
readonly values = new Map<SnakeCaseClientNameEvents, RunData<any>[]>();
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<T extends SnakeCaseClientNameEvents>(options: RunData<T>['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<any>['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<T extends GatewayEvents>(name: T, data: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>) {
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<SnakeCase<ClientNameEvents>>;
type RunData<T extends SnakeCaseClientNameEvents> = {
options: {
event: T;
idle?: number;
timeout?: number;
onStop?: (reason: string) => unknown;
onStopError?: (reason: string, error: unknown) => unknown;
filter: (arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>) => Awaitable<boolean>;
run: (arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>, stop: (reason?: string) => void) => unknown;
onRunError?: (
arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>,
error: unknown,
stop: (reason?: string) => void,
) => unknown;
};
idle?: NodeJS.Timeout;
timeout?: NodeJS.Timeout;
nonce: string;
};
export class Collectors {
readonly values = new Map<SnakeCaseClientNameEvents, RunData<any>[]>();
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<T extends SnakeCaseClientNameEvents>(options: RunData<T>['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<any>['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<T extends GatewayEvents>(name: T, data: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>) {
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;
}
}
}
}

View File

@ -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> = N extends
| ApplicationCommandOptionType.Subcommand
| ApplicationCommandOptionType.SubcommandGroup
? never
: {
required?: boolean;
value?(
data: { context: CommandContext; value: ReturnOptionsTypes[N] },
ok: OKFunction<any>,
fail: StopFunction,
): 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[];
/** @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<any> | 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<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;
}
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> = N extends
| ApplicationCommandOptionType.Subcommand
| ApplicationCommandOptionType.SubcommandGroup
? never
: {
required?: boolean;
value?(
data: { context: CommandContext; value: ReturnOptionsTypes[N] },
ok: OKFunction<any>,
fail: StopFunction,
): 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[];
/** @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<any> | 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<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;
}

View File

@ -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) {

View File

@ -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<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components' | 'poll' | 'embeds'>> {}
export class BaseMessage extends DiscordBase {
guildId: string | undefined;
timestamp?: number;
author!: User;
member?: GuildMember;
components: MessageActionRowComponent<ActionRowMessageComponents>[];
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<APIGuildMember, 'user'> }).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<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components' | 'poll' | 'embeds'>> {}
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<MessageCreateBodyRequest, 'message_reference'>, 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<MessageWebhookMethodEditParams, 'messageId'>['body'] &
Pick<MessageWebhookMethodEditParams, 'query'>;
export type WriteMessageWebhook = MessageWebhookMethodWriteParams['body'] &
Pick<MessageWebhookMethodWriteParams, 'query'>;
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<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components' | 'poll' | 'embeds'>> {}
export class BaseMessage extends DiscordBase {
guildId: string | undefined;
timestamp?: number;
author!: User;
member?: GuildMember;
components: MessageActionRowComponent<ActionRowMessageComponents>[];
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<APIGuildMember, 'user'> }).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<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components' | 'poll' | 'embeds'>> {}
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<MessageCreateBodyRequest, 'message_reference'>, 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<MessageWebhookMethodEditParams, 'messageId'>['body'] &
Pick<MessageWebhookMethodEditParams, 'query'>;
export type WriteMessageWebhook = MessageWebhookMethodWriteParams['body'] &
Pick<MessageWebhookMethodWriteParams, 'query'>;
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 };
}
}