feat: Component Context (#166)

* feat: component context
This commit is contained in:
Marcos Susaña 2024-03-27 19:46:28 -04:00 committed by GitHub
parent 89c525d052
commit 776c604b3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 205 additions and 65 deletions

View File

@ -30,6 +30,7 @@ import { ComponentHandler, type ComponentHandlerLike } from '../components/handl
import { LangsHandler, type LangsHandlerLike } from '../langs/handler'; import { LangsHandler, type LangsHandlerLike } from '../langs/handler';
import type { import type {
ChatInputCommandInteraction, ChatInputCommandInteraction,
ComponentInteraction,
Message, Message,
MessageCommandInteraction, MessageCommandInteraction,
UserCommandInteraction, UserCommandInteraction,
@ -284,6 +285,7 @@ export interface BaseClientOptions {
| ChatInputCommandInteraction<boolean> | ChatInputCommandInteraction<boolean>
| UserCommandInteraction<boolean> | UserCommandInteraction<boolean>
| MessageCommandInteraction<boolean> | MessageCommandInteraction<boolean>
| ComponentInteraction
| When<InferWithPrefix, Message, never>, | When<InferWithPrefix, Message, never>,
) => {}; ) => {};
globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[]; globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[];

View File

@ -1,14 +1,4 @@
import { import type { AllChannels, Guild, InferWithPrefix, ReturnCache, WebhookMessage } from '../..';
MenuCommandContext,
User,
type AllChannels,
type Guild,
type InferWithPrefix,
type MessageCommandInteraction,
type ReturnCache,
type UserCommandInteraction,
type WebhookMessage,
} from '../..';
import type { Client, WorkerClient } from '../../client'; import type { Client, WorkerClient } from '../../client';
import { MessageFlags, type If, type UnionToTuple, type When } from '../../common'; import { MessageFlags, type If, type UnionToTuple, type When } from '../../common';
import type { InteractionCreateBodyRequest, InteractionMessageUpdateBodyRequest } from '../../common/types/write'; import type { InteractionCreateBodyRequest, InteractionMessageUpdateBodyRequest } from '../../common/types/write';
@ -18,15 +8,20 @@ import {
type GuildMember, type GuildMember,
type InteractionGuildMember, type InteractionGuildMember,
} from '../../structures'; } from '../../structures';
import { BaseContext } from '../basecontex';
import type { RegisteredMiddlewares } from '../decorators'; import type { RegisteredMiddlewares } from '../decorators';
import type { OptionResolver } from '../optionresolver'; import type { OptionResolver } from '../optionresolver';
import type { Command, ContextOptions, OptionsRecord, SubCommand } from './chat'; import type { Command, ContextOptions, OptionsRecord, SubCommand } from './chat';
import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared'; import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared';
export interface CommandContext<T extends OptionsRecord = {}, M extends keyof RegisteredMiddlewares = never> export interface CommandContext<T extends OptionsRecord = {}, M extends keyof RegisteredMiddlewares = never>
extends ExtendContext {} extends BaseContext,
ExtendContext {}
export class CommandContext<T extends OptionsRecord = {}, M extends keyof RegisteredMiddlewares = never> { export class CommandContext<
T extends OptionsRecord = {},
M extends keyof RegisteredMiddlewares = never,
> extends BaseContext {
message!: If<InferWithPrefix, Message | undefined, undefined>; message!: If<InferWithPrefix, Message | undefined, undefined>;
interaction!: If<InferWithPrefix, ChatInputCommandInteraction | undefined, ChatInputCommandInteraction>; interaction!: If<InferWithPrefix, ChatInputCommandInteraction | undefined, ChatInputCommandInteraction>;
@ -38,6 +33,7 @@ export class CommandContext<T extends OptionsRecord = {}, M extends keyof Regist
readonly shardId: number, readonly shardId: number,
readonly command: Command | SubCommand, readonly command: Command | SubCommand,
) { ) {
super(client);
if (data instanceof Message) { if (data instanceof Message) {
this.message = data as never; this.message = data as never;
} else { } else {
@ -54,7 +50,7 @@ export class CommandContext<T extends OptionsRecord = {}, M extends keyof Regist
} }
get t() { get t() {
return this.client.langs!.get(this.interaction?.locale ?? this.client.langs!.defaultLang ?? 'en-US'); return this.client.t(this.interaction?.locale ?? this.client.langs?.defaultLang ?? 'en-US');
} }
get fullCommandName() { get fullCommandName() {
@ -180,20 +176,4 @@ export class CommandContext<T extends OptionsRecord = {}, M extends keyof Regist
> { > {
return this.interaction?.member || ((this.message! as Message)?.member as any); return this.interaction?.member || ((this.message! as Message)?.member as any);
} }
isChat(): this is CommandContext {
return this instanceof CommandContext;
}
isMenu(): this is MenuCommandContext<any> {
return this instanceof MenuCommandContext;
}
isMenuUser(): this is MenuCommandContext<UserCommandInteraction> {
return this instanceof MenuCommandContext && this.target instanceof User;
}
isMenuMessage(): this is MenuCommandContext<MessageCommandInteraction> {
return this instanceof MenuCommandContext && this.target instanceof Message;
}
} }

View File

@ -1,4 +1,4 @@
import { CommandContext, type ContextMenuCommand, type ReturnCache, type WebhookMessage } from '../..'; import type { ContextMenuCommand, ReturnCache, WebhookMessage } from '../..';
import { import {
ApplicationCommandType, ApplicationCommandType,
MessageFlags, MessageFlags,
@ -17,6 +17,7 @@ import {
type MessageCommandInteraction, type MessageCommandInteraction,
type UserCommandInteraction, type UserCommandInteraction,
} from '../../structures'; } from '../../structures';
import { BaseContext } from '../basecontex';
import type { RegisteredMiddlewares } from '../decorators'; import type { RegisteredMiddlewares } from '../decorators';
import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared'; import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared';
@ -25,26 +26,25 @@ export type InteractionTarget<T> = T extends MessageCommandInteraction ? Message
export interface MenuCommandContext< export interface MenuCommandContext<
T extends MessageCommandInteraction | UserCommandInteraction, T extends MessageCommandInteraction | UserCommandInteraction,
M extends keyof RegisteredMiddlewares = never, M extends keyof RegisteredMiddlewares = never,
> extends ExtendContext {} > extends BaseContext,
ExtendContext {}
export class MenuCommandContext< export class MenuCommandContext<
T extends MessageCommandInteraction | UserCommandInteraction, T extends MessageCommandInteraction | UserCommandInteraction,
M extends keyof RegisteredMiddlewares = never, M extends keyof RegisteredMiddlewares = never,
> { > extends BaseContext {
constructor( constructor(
readonly client: UsingClient, readonly client: UsingClient,
readonly interaction: T, readonly interaction: T,
readonly shardId: number, readonly shardId: number,
readonly command: ContextMenuCommand, readonly command: ContextMenuCommand,
) {} ) {
super(client);
}
metadata: CommandMetadata<UnionToTuple<M>> = {} as never; metadata: CommandMetadata<UnionToTuple<M>> = {} as never;
globalMetadata: GlobalMetadata = {}; globalMetadata: GlobalMetadata = {};
get proxy() {
return this.client.proxy;
}
// biome-ignore lint/suspicious/useGetterReturn: default don't exist. // biome-ignore lint/suspicious/useGetterReturn: default don't exist.
get target(): InteractionTarget<T> { get target(): InteractionTarget<T> {
switch (this.interaction.data.type) { switch (this.interaction.data.type) {
@ -152,20 +152,4 @@ export class MenuCommandContext<
get member() { get member() {
return this.interaction.member; return this.interaction.member;
} }
isChat(): this is CommandContext {
return this instanceof CommandContext;
}
isMenu(): this is MenuCommandContext<any> {
return this instanceof MenuCommandContext;
}
isMenuUser(): this is MenuCommandContext<UserCommandInteraction> {
return this instanceof MenuCommandContext && this.target instanceof User;
}
isMenuMessage(): this is MenuCommandContext<MessageCommandInteraction> {
return this instanceof MenuCommandContext && this.target instanceof Message;
}
} }

View File

@ -0,0 +1,33 @@
import { ComponentContext, type ComponentCommandInteractionMap } from '../components/componentcontext';
import { Message, User, type MessageCommandInteraction, type UserCommandInteraction } from '../structures';
import { CommandContext } from './applications/chatcontext';
import { MenuCommandContext } from './applications/menucontext';
import type { UsingClient } from './applications/shared';
export class BaseContext {
constructor(readonly client: UsingClient) {}
get proxy() {
return this.client.proxy;
}
isChat(): this is CommandContext {
return this instanceof CommandContext;
}
isMenu(): this is MenuCommandContext<any> {
return this instanceof MenuCommandContext;
}
isMenuUser(): this is MenuCommandContext<UserCommandInteraction> {
return this instanceof MenuCommandContext && this.target instanceof User;
}
isMenuMessage(): this is MenuCommandContext<MessageCommandInteraction> {
return this instanceof MenuCommandContext && this.target instanceof Message;
}
isComponent(): this is ComponentContext<keyof ComponentCommandInteractionMap> {
return this instanceof ComponentContext;
}
}

View File

@ -1,5 +1,6 @@
import type { ComponentType } from 'discord-api-types/v10'; import { ComponentType } from 'discord-api-types/v10';
import type { ComponentInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from '../structures'; import type { ModalSubmitInteraction } from '../structures';
import type { ComponentCommandInteractionMap, ComponentContext } from './componentcontext';
export const InteractionCommandType = { export const InteractionCommandType = {
COMPONENT: 0, COMPONENT: 0,
@ -12,9 +13,14 @@ export interface ComponentCommand {
export abstract class ComponentCommand { export abstract class ComponentCommand {
type = InteractionCommandType.COMPONENT; type = InteractionCommandType.COMPONENT;
abstract componentType: ComponentType; abstract componentType: keyof ComponentCommandInteractionMap;
abstract filter(interaction: ComponentInteraction | StringSelectMenuInteraction): Promise<boolean> | boolean; abstract filter(interaction: ComponentContext<typeof this.componentType>): Promise<boolean> | boolean;
abstract run(interaction: ComponentInteraction | StringSelectMenuInteraction): any; abstract run(interaction: ComponentContext<typeof this.componentType>): any;
get cType(): number {
// @ts-expect-error
return ComponentType[this.componentType];
}
} }
export interface ModalCommand { export interface ModalCommand {

View File

@ -0,0 +1,133 @@
import { MessageFlags } from 'discord-api-types/v10';
import type {
AllChannels,
ButtonInteraction,
ChannelSelectMenuInteraction,
ComponentInteraction,
Guild,
GuildMember,
MentionableSelectMenuInteraction,
Message,
ReturnCache,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserSelectMenuInteraction,
WebhookMessage,
} from '..';
import type { ExtendContext, UsingClient } from '../commands';
import { BaseContext } from '../commands/basecontex';
import type { InteractionCreateBodyRequest, InteractionMessageUpdateBodyRequest, When } from '../common';
export interface ComponentContext<Type extends keyof ComponentCommandInteractionMap>
extends BaseContext,
ExtendContext {}
export class ComponentContext<Type extends keyof ComponentCommandInteractionMap> extends BaseContext {
constructor(
readonly client: UsingClient,
public interaction: ComponentCommandInteractionMap[Type] | ComponentInteraction,
) {
super(client);
}
get proxy() {
return this.client.proxy;
}
get t() {
return this.client.langs!.get(this.interaction?.locale ?? this.client.langs?.defaultLang ?? 'en-US');
}
get customId() {
return this.interaction.customId;
}
get write() {
return this.interaction.write;
}
deferReply(ephemeral = false) {
return this.interaction.deferReply(ephemeral ? MessageFlags.Ephemeral : undefined);
}
get editResponse() {
return this.interaction.editResponse;
}
get update() {
return this.interaction.update;
}
editOrReply<FR extends boolean = false>(
body: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest,
fetchReply?: FR,
): Promise<When<FR, WebhookMessage | Message, void | WebhookMessage | Message>> {
return this.interaction.editOrReply(body as InteractionCreateBodyRequest, fetchReply);
}
deleteResponse() {
return this.interaction.deleteResponse();
}
channel(mode?: 'rest' | 'flow'): Promise<AllChannels>;
channel(mode?: 'cache'): ReturnCache<AllChannels>;
channel(mode: 'cache' | 'rest' | 'flow' = 'cache') {
if (this.interaction?.channel && mode === 'cache')
return this.client.cache.adapter.isAsync ? Promise.resolve(this.interaction.channel) : this.interaction.channel;
return this.client.channels.fetch(this.channelId, mode === 'rest');
}
me(mode?: 'rest' | 'flow'): Promise<GuildMember>;
me(mode?: 'cache'): ReturnCache<GuildMember | undefined>;
me(mode: 'cache' | 'rest' | 'flow' = 'cache') {
if (!this.guildId)
return mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve();
switch (mode) {
case 'cache':
return this.client.cache.members?.get(this.client.botId, this.guildId);
default:
return this.client.members.fetch(this.guildId, this.client.botId, mode === 'rest');
}
}
guild(mode?: 'rest' | 'flow'): Promise<Guild<'cached' | 'api'> | undefined>;
guild(mode?: 'cache'): ReturnCache<Guild<'cached'> | undefined>;
guild(mode: 'cache' | 'rest' | 'flow' = 'cache') {
if (!this.guildId)
return (
mode === 'cache' ? (this.client.cache.adapter.isAsync ? Promise.resolve() : undefined) : Promise.resolve()
) as any;
switch (mode) {
case 'cache':
return this.client.cache.guilds?.get(this.guildId);
default:
return this.client.guilds.fetch(this.guildId, mode === 'rest');
}
}
get guildId() {
return this.interaction.guildId;
}
get channelId() {
return this.interaction.channelId!;
}
get author() {
return this.interaction.user;
}
get member() {
return this.interaction.member;
}
}
export interface ComponentCommandInteractionMap {
ActionRow: never;
Button: ButtonInteraction;
StringSelect: StringSelectMenuInteraction;
TextInput: never;
UserSelect: UserSelectMenuInteraction;
RoleSelect: RoleSelectMenuInteraction;
MentioableSelect: MentionableSelectMenuInteraction;
ChannelSelect: ChannelSelectMenuInteraction;
}

View File

@ -4,6 +4,7 @@ import type { UsingClient } from '../commands';
import { BaseHandler, magicImport, type Logger, type OnFailCallback } from '../common'; import { BaseHandler, magicImport, type Logger, type OnFailCallback } from '../common';
import type { ComponentInteraction, ModalSubmitInteraction } from '../structures'; import type { ComponentInteraction, ModalSubmitInteraction } from '../structures';
import { ComponentCommand, InteractionCommandType, ModalCommand } from './command'; import { ComponentCommand, InteractionCommandType, ModalCommand } from './command';
import { ComponentContext } from './componentcontext';
type COMPONENTS = { type COMPONENTS = {
components: { match: string | string[] | RegExp; callback: ComponentCallback }[]; components: { match: string | string[] | RegExp; callback: ComponentCallback }[];
@ -218,12 +219,13 @@ export class ComponentHandler extends BaseHandler {
async executeComponent(interaction: ComponentInteraction) { async executeComponent(interaction: ComponentInteraction) {
for (const i of this.commands) { for (const i of this.commands) {
try { try {
if ( if (i.type === InteractionCommandType.COMPONENT && i.cType === interaction.componentType) {
i.type === InteractionCommandType.COMPONENT && const context = new ComponentContext(this.client, interaction);
i.componentType === interaction.componentType && const extended = this.client.options?.context?.(interaction) ?? {};
(await i.filter(interaction)) Object.assign(context, extended);
) { // @ts-expect-error
await i.run(interaction); if (!(await i.filter(interaction))) continue;
await i.run(context);
break; break;
} }
} catch (e) { } catch (e) {