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 type {
ChatInputCommandInteraction,
ComponentInteraction,
Message,
MessageCommandInteraction,
UserCommandInteraction,
@ -284,6 +285,7 @@ export interface BaseClientOptions {
| ChatInputCommandInteraction<boolean>
| UserCommandInteraction<boolean>
| MessageCommandInteraction<boolean>
| ComponentInteraction
| When<InferWithPrefix, Message, never>,
) => {};
globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[];

View File

@ -1,14 +1,4 @@
import {
MenuCommandContext,
User,
type AllChannels,
type Guild,
type InferWithPrefix,
type MessageCommandInteraction,
type ReturnCache,
type UserCommandInteraction,
type WebhookMessage,
} from '../..';
import type { AllChannels, Guild, InferWithPrefix, ReturnCache, WebhookMessage } from '../..';
import type { Client, WorkerClient } from '../../client';
import { MessageFlags, type If, type UnionToTuple, type When } from '../../common';
import type { InteractionCreateBodyRequest, InteractionMessageUpdateBodyRequest } from '../../common/types/write';
@ -18,15 +8,20 @@ import {
type GuildMember,
type InteractionGuildMember,
} from '../../structures';
import { BaseContext } from '../basecontex';
import type { RegisteredMiddlewares } from '../decorators';
import type { OptionResolver } from '../optionresolver';
import type { Command, ContextOptions, OptionsRecord, SubCommand } from './chat';
import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared';
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>;
interaction!: If<InferWithPrefix, ChatInputCommandInteraction | undefined, ChatInputCommandInteraction>;
@ -38,6 +33,7 @@ export class CommandContext<T extends OptionsRecord = {}, M extends keyof Regist
readonly shardId: number,
readonly command: Command | SubCommand,
) {
super(client);
if (data instanceof Message) {
this.message = data as never;
} else {
@ -54,7 +50,7 @@ export class CommandContext<T extends OptionsRecord = {}, M extends keyof Regist
}
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() {
@ -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);
}
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 {
ApplicationCommandType,
MessageFlags,
@ -17,6 +17,7 @@ import {
type MessageCommandInteraction,
type UserCommandInteraction,
} from '../../structures';
import { BaseContext } from '../basecontex';
import type { RegisteredMiddlewares } from '../decorators';
import type { CommandMetadata, ExtendContext, GlobalMetadata, UsingClient } from './shared';
@ -25,26 +26,25 @@ export type InteractionTarget<T> = T extends MessageCommandInteraction ? Message
export interface MenuCommandContext<
T extends MessageCommandInteraction | UserCommandInteraction,
M extends keyof RegisteredMiddlewares = never,
> extends ExtendContext {}
> extends BaseContext,
ExtendContext {}
export class MenuCommandContext<
T extends MessageCommandInteraction | UserCommandInteraction,
M extends keyof RegisteredMiddlewares = never,
> {
> extends BaseContext {
constructor(
readonly client: UsingClient,
readonly interaction: T,
readonly shardId: number,
readonly command: ContextMenuCommand,
) {}
) {
super(client);
}
metadata: CommandMetadata<UnionToTuple<M>> = {} as never;
globalMetadata: GlobalMetadata = {};
get proxy() {
return this.client.proxy;
}
// biome-ignore lint/suspicious/useGetterReturn: default don't exist.
get target(): InteractionTarget<T> {
switch (this.interaction.data.type) {
@ -152,20 +152,4 @@ export class MenuCommandContext<
get 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 type { ComponentInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from '../structures';
import { ComponentType } from 'discord-api-types/v10';
import type { ModalSubmitInteraction } from '../structures';
import type { ComponentCommandInteractionMap, ComponentContext } from './componentcontext';
export const InteractionCommandType = {
COMPONENT: 0,
@ -12,9 +13,14 @@ export interface ComponentCommand {
export abstract class ComponentCommand {
type = InteractionCommandType.COMPONENT;
abstract componentType: ComponentType;
abstract filter(interaction: ComponentInteraction | StringSelectMenuInteraction): Promise<boolean> | boolean;
abstract run(interaction: ComponentInteraction | StringSelectMenuInteraction): any;
abstract componentType: keyof ComponentCommandInteractionMap;
abstract filter(interaction: ComponentContext<typeof this.componentType>): Promise<boolean> | boolean;
abstract run(interaction: ComponentContext<typeof this.componentType>): any;
get cType(): number {
// @ts-expect-error
return ComponentType[this.componentType];
}
}
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 type { ComponentInteraction, ModalSubmitInteraction } from '../structures';
import { ComponentCommand, InteractionCommandType, ModalCommand } from './command';
import { ComponentContext } from './componentcontext';
type COMPONENTS = {
components: { match: string | string[] | RegExp; callback: ComponentCallback }[];
@ -218,12 +219,13 @@ export class ComponentHandler extends BaseHandler {
async executeComponent(interaction: ComponentInteraction) {
for (const i of this.commands) {
try {
if (
i.type === InteractionCommandType.COMPONENT &&
i.componentType === interaction.componentType &&
(await i.filter(interaction))
) {
await i.run(interaction);
if (i.type === InteractionCommandType.COMPONENT && i.cType === interaction.componentType) {
const context = new ComponentContext(this.client, interaction);
const extended = this.client.options?.context?.(interaction) ?? {};
Object.assign(context, extended);
// @ts-expect-error
if (!(await i.filter(interaction))) continue;
await i.run(context);
break;
}
} catch (e) {