import { type APIActionRowComponent, type APIApplicationCommandAutocompleteInteraction, type APIApplicationCommandInteraction, type APIBaseInteraction, type APIChatInputApplicationCommandInteraction, type APIChatInputApplicationCommandInteractionData, type APICommandAutocompleteInteractionResponseCallbackData, type APIInteraction, type APIInteractionResponse, type APIInteractionResponseChannelMessageWithSource, type APIInteractionResponseDeferredChannelMessageWithSource, type APIInteractionResponseDeferredMessageUpdate, type APIInteractionResponsePong, type APIInteractionResponseUpdateMessage, type APIMessageApplicationCommandInteraction, type APIMessageApplicationCommandInteractionData, type APIMessageButtonInteractionData, type APIMessageChannelSelectInteractionData, type APIMessageComponentInteraction, type APIMessageComponentSelectMenuInteraction, type APIMessageMentionableSelectInteractionData, type APIMessageRoleSelectInteractionData, type APIMessageStringSelectInteractionData, type APIMessageUserSelectInteractionData, type APIModalSubmission, type APIModalSubmitInteraction, type APITextInputComponent, type APIUserApplicationCommandInteraction, type APIUserApplicationCommandInteractionData, ApplicationCommandType, ComponentType, type GatewayInteractionCreateDispatchData, InteractionResponseType, InteractionType, type MessageFlags, type RESTPostAPIInteractionCallbackJSONBody, type RESTAPIAttachment, } from '../types'; import type { RawFile } from '../api'; import { ActionRow, Embed, Modal, PollBuilder, resolveAttachment, resolveFiles } from '../builders'; import type { ContextOptionsResolved, UsingClient } from '../commands'; import type { ObjectToLower, OmitInsert, ToClass, When, ComponentInteractionMessageUpdate, InteractionCreateBodyRequest, InteractionMessageUpdateBodyRequest, MessageCreateBodyRequest, MessageUpdateBodyRequest, MessageWebhookCreateBodyRequest, ModalCreateBodyRequest, } from '../common'; import type { AllChannels } from './'; import channelFrom from './channels'; import { DiscordBase } from './extra/DiscordBase'; import { PermissionsBitField } from './extra/Permissions'; import { type GuildRoleStructure, type InteractionGuildMemberStructure, type MessageStructure, Transformers, type UserStructure, type WebhookMessageStructure, type OptionResolverStructure, } from '../client/transformers'; import { mix } from '../deps/mixer'; export type ReplyInteractionBody = | { type: InteractionResponseType.Modal; data: ModalCreateBodyRequest } | { type: InteractionResponseType.ChannelMessageWithSource | InteractionResponseType.UpdateMessage; data: InteractionCreateBodyRequest | InteractionMessageUpdateBodyRequest | ComponentInteractionMessageUpdate; } | Exclude; /** @internal */ export type __InternalReplyFunction = (_: { body: APIInteractionResponse; files?: RawFile[] }) => Promise; export interface BaseInteraction extends ObjectToLower< Omit< APIBaseInteraction, 'user' | 'member' | 'message' | 'channel' | 'type' | 'app_permissions' > > {} export class BaseInteraction< FromGuild extends boolean = boolean, Type extends APIInteraction = APIInteraction, > extends DiscordBase { user: UserStructure; member!: When; channel?: AllChannels; message?: MessageStructure; replied?: Promise | boolean; appPermissions?: PermissionsBitField; constructor( readonly client: UsingClient, interaction: Type, protected __reply?: __InternalReplyFunction, ) { super(client, interaction); if (interaction.member) { this.member = Transformers.InteractionGuildMember( client, interaction.member, interaction.member.user, interaction.guild_id!, ) as never; } if (interaction.message) { this.message = Transformers.Message(client, interaction.message); } if (interaction.app_permissions) { this.appPermissions = new PermissionsBitField(Number(interaction.app_permissions)); } if (interaction.channel) { this.channel = channelFrom(interaction.channel, client); } this.user = this.member?.user ?? Transformers.User(client, interaction.user!); } static transformBodyRequest( body: ReplyInteractionBody, files: RawFile[] | undefined, self: UsingClient, ): APIInteractionResponse { switch (body.type) { case InteractionResponseType.ApplicationCommandAutocompleteResult: case InteractionResponseType.DeferredMessageUpdate: case InteractionResponseType.DeferredChannelMessageWithSource: return body; case InteractionResponseType.ChannelMessageWithSource: case InteractionResponseType.UpdateMessage: { return { type: body.type, //@ts-ignore data: BaseInteraction.transformBodyRequest(body.data ?? {}, files, self), }; } case InteractionResponseType.Modal: return { type: body.type, data: body.data instanceof Modal ? body.data.toJSON() : { ...body.data, components: body.data?.components ? body.data.components.map(x => x instanceof ActionRow ? (x.toJSON() as unknown as APIActionRowComponent) : x, ) : [], }, }; default: return body; } } static transformBody( body: | InteractionMessageUpdateBodyRequest | MessageUpdateBodyRequest | MessageCreateBodyRequest | MessageWebhookCreateBodyRequest, files: RawFile[] | undefined, self: UsingClient, ) { const poll = (body as MessageWebhookCreateBodyRequest).poll; const allow = { allowed_mentions: self.options?.allowedMentions, ...body, components: body.components?.map(x => (x instanceof ActionRow ? x.toJSON() : x)), embeds: body?.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)), poll: poll ? (poll instanceof PollBuilder ? poll.toJSON() : poll) : undefined, }; if ('attachment' in body) { allow.attachments = body.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x), })) ?? undefined; } else if (files?.length) { allow.attachments = files?.map((x, id) => ({ id, filename: x.name, })) as RESTAPIAttachment[]; } return allow as unknown as T; } private async matchReplied(body: ReplyInteractionBody) { if (this.__reply) { //@ts-expect-error const { files, ...rest } = body.data ?? {}; //@ts-expect-error const data = body.data instanceof Modal ? body.data : rest; const parsedFiles = files ? await resolveFiles(files) : undefined; return (this.replied = this.__reply({ body: BaseInteraction.transformBodyRequest({ data, type: body.type }, parsedFiles, this.client), files: parsedFiles, }).then(() => (this.replied = true))); } return (this.replied = this.client.interactions.reply(this.id, this.token, body).then(() => (this.replied = true))); } async reply(body: ReplyInteractionBody) { if (this.replied) { throw new Error('Interaction already replied'); } await this.matchReplied(body); // @ts-expect-error if (body.data instanceof Modal && body.data.__exec) // @ts-expect-error this.client.components.modals.set(this.user.id, (body.data as Modal).__exec); } deferReply(flags?: MessageFlags) { return this.reply({ type: InteractionResponseType.DeferredChannelMessageWithSource, data: { flags, }, }); } static from(client: UsingClient, gateway: GatewayInteractionCreateDispatchData, __reply?: __InternalReplyFunction) { switch (gateway.type) { case InteractionType.ApplicationCommandAutocomplete: return new AutocompleteInteraction(client, gateway, undefined, __reply); // biome-ignore lint/suspicious/noFallthroughSwitchClause: bad interaction between biome and ts-server case InteractionType.ApplicationCommand: switch (gateway.data.type) { case ApplicationCommandType.ChatInput: return new ChatInputCommandInteraction( client, gateway as APIChatInputApplicationCommandInteraction, __reply, ); case ApplicationCommandType.User: return new UserCommandInteraction(client, gateway as APIUserApplicationCommandInteraction, __reply); case ApplicationCommandType.Message: return new MessageCommandInteraction(client, gateway as APIMessageApplicationCommandInteraction, __reply); } // biome-ignore lint/suspicious/noFallthroughSwitchClause: bad interaction between biome and ts-server case InteractionType.MessageComponent: switch (gateway.data.component_type) { case ComponentType.Button: return new ButtonInteraction(client, gateway as APIMessageComponentInteraction, __reply); case ComponentType.ChannelSelect: return new ChannelSelectMenuInteraction( client, gateway as APIMessageComponentSelectMenuInteraction, __reply, ); case ComponentType.RoleSelect: return new RoleSelectMenuInteraction(client, gateway as APIMessageComponentSelectMenuInteraction, __reply); case ComponentType.MentionableSelect: return new MentionableSelectMenuInteraction( client, gateway as APIMessageComponentSelectMenuInteraction, __reply, ); case ComponentType.UserSelect: return new UserSelectMenuInteraction(client, gateway as APIMessageComponentSelectMenuInteraction, __reply); case ComponentType.StringSelect: return new StringSelectMenuInteraction( client, gateway as APIMessageComponentSelectMenuInteraction, __reply, ); } case InteractionType.ModalSubmit: return new ModalSubmitInteraction(client, gateway); default: return new BaseInteraction(client, gateway); } } fetchGuild(force = false) { return this.guildId ? this.client.guilds.fetch(this.guildId, force) : undefined; } } export type AllInteractions = | AutocompleteInteraction | ChatInputCommandInteraction | UserCommandInteraction | MessageCommandInteraction | ComponentInteraction | SelectMenuInteraction | ModalSubmitInteraction | BaseInteraction; export interface AutocompleteInteraction extends ObjectToLower< Omit< APIApplicationCommandAutocompleteInteraction, 'user' | 'member' | 'type' | 'data' | 'message' | 'channel' | 'app_permissions' > > {} export class AutocompleteInteraction extends BaseInteraction< FromGuild, APIApplicationCommandAutocompleteInteraction > { declare type: InteractionType.ApplicationCommandAutocomplete; declare data: ObjectToLower; options: OptionResolverStructure; constructor( client: UsingClient, interaction: APIApplicationCommandAutocompleteInteraction, resolver?: OptionResolverStructure, protected __reply?: __InternalReplyFunction, ) { super(client, interaction); this.options = resolver ?? Transformers.OptionResolver( client, interaction.data.options, undefined, interaction.guild_id, interaction.data.resolved as ContextOptionsResolved, ); } getInput() { return this.options.getAutocompleteValue() ?? ''; } respond(choices: APICommandAutocompleteInteractionResponseCallbackData['choices']) { return super.reply({ data: { choices }, type: InteractionResponseType.ApplicationCommandAutocompleteResult }); } /** @intenal */ async reply(..._args: unknown[]) { throw new Error('Cannot use reply in this interaction'); } } export class Interaction< FromGuild extends boolean = boolean, Type extends APIInteraction = APIInteraction, > extends BaseInteraction { fetchMessage(messageId: string) { return this.client.interactions.fetchResponse(this.token, messageId); } fetchResponse() { return this.fetchMessage('@original'); } async write( body: InteractionCreateBodyRequest, fetchReply?: FR, ): Promise> { (await this.reply({ type: InteractionResponseType.ChannelMessageWithSource, data: body, })) as never; if (fetchReply) return this.fetchResponse() as never; return undefined as never; } modal(body: ModalCreateBodyRequest) { return this.reply({ type: InteractionResponseType.Modal, data: body, }); } async editOrReply( body: InteractionCreateBodyRequest, fetchReply?: FR, ): Promise>; async editOrReply(body: InteractionMessageUpdateBodyRequest, fetchReply?: FR) { if (await this.replied) { const { content, embeds, allowed_mentions, components, files, attachments } = body; return this.editResponse({ content, embeds, allowed_mentions, components, files, attachments }); } return this.write(body as InteractionCreateBodyRequest, fetchReply); } editMessage(messageId: string, body: InteractionMessageUpdateBodyRequest) { return this.client.interactions.editMessage(this.token, messageId, body); } editResponse(body: InteractionMessageUpdateBodyRequest) { return this.editMessage('@original', body); } deleteResponse() { return this.deleteMessage('@original'); } deleteMessage(messageId: string) { return this.client.interactions.deleteResponse(this.id, this.token, messageId); } async followup(body: MessageWebhookCreateBodyRequest) { return this.client.interactions.followup(this.token, body); } } export class ApplicationCommandInteraction< FromGuild extends boolean = boolean, Type extends APIApplicationCommandInteraction = APIApplicationCommandInteraction, > extends Interaction { type = ApplicationCommandType.ChatInput; respond( data: | APIInteractionResponseChannelMessageWithSource | APIInteractionResponseDeferredChannelMessageWithSource | APIInteractionResponseDeferredMessageUpdate | APIInteractionResponseUpdateMessage, ) { return this.reply(data); } } export interface ComponentInteraction extends ObjectToLower< Omit< APIMessageComponentInteraction, 'user' | 'member' | 'type' | 'data' | 'message' | 'channel' | 'app_permissions' > > {} export class ComponentInteraction< FromGuild extends boolean = boolean, Type extends APIMessageComponentInteraction = APIMessageComponentInteraction, > extends Interaction { declare data: ObjectToLower; declare channelId: string; declare channel: AllChannels; declare type: InteractionType.MessageComponent; declare message: MessageStructure; update(data: ComponentInteractionMessageUpdate) { return this.reply({ type: InteractionResponseType.UpdateMessage, data, }); } deferUpdate() { return this.reply({ type: InteractionResponseType.DeferredMessageUpdate, }); } get customId() { return this.data.customId; } get componentType() { return this.data.componentType; } isButton(): this is ButtonInteraction { return this.data.componentType === ComponentType.Button; } isChannelSelectMenu(): this is ChannelSelectMenuInteraction { return this.componentType === ComponentType.ChannelSelect; } isRoleSelectMenu(): this is RoleSelectMenuInteraction { return this.componentType === ComponentType.RoleSelect; } isMentionableSelectMenu(): this is MentionableSelectMenuInteraction { return this.componentType === ComponentType.MentionableSelect; } isUserSelectMenu(): this is UserSelectMenuInteraction { return this.componentType === ComponentType.UserSelect; } isStringSelectMenu(): this is StringSelectMenuInteraction { return this.componentType === ComponentType.StringSelect; } } export class ButtonInteraction extends ComponentInteraction { declare data: ObjectToLower; } export class SelectMenuInteraction extends ComponentInteraction { declare data: ObjectToLower; constructor( client: UsingClient, interaction: APIMessageComponentSelectMenuInteraction, protected __reply?: __InternalReplyFunction, ) { super(client, interaction); } get values() { return this.data.values; } } export class StringSelectMenuInteraction< T extends any[] = string[], > extends (SelectMenuInteraction as unknown as ToClass< Omit, StringSelectMenuInteraction >) { declare data: OmitInsert, 'values', { values: T }>; declare values: T; } export class ChannelSelectMenuInteraction extends SelectMenuInteraction { channels: AllChannels[]; constructor( client: UsingClient, interaction: APIMessageComponentSelectMenuInteraction, protected __reply?: __InternalReplyFunction, ) { super(client, interaction); const resolved = (interaction.data as APIMessageChannelSelectInteractionData).resolved; this.channels = this.values.map(x => channelFrom(resolved.channels[x], this.client)); } } export class MentionableSelectMenuInteraction extends SelectMenuInteraction { roles: GuildRoleStructure[]; members: InteractionGuildMemberStructure[]; users: UserStructure[]; constructor( client: UsingClient, interaction: APIMessageComponentSelectMenuInteraction, protected __reply?: __InternalReplyFunction, ) { super(client, interaction); const resolved = (interaction.data as APIMessageMentionableSelectInteractionData).resolved; this.roles = resolved.roles ? this.values.map(x => Transformers.GuildRole(this.client, resolved.roles![x], this.guildId!)) : []; this.members = resolved.members ? this.values.map(x => Transformers.InteractionGuildMember( this.client, resolved.members![x], resolved.users![this.values!.find(u => u === x)!]!, this.guildId!, ), ) : []; this.users = resolved.users ? this.values.map(x => Transformers.User(this.client, resolved.users![x])) : []; } } export class RoleSelectMenuInteraction extends SelectMenuInteraction { roles: GuildRoleStructure[]; constructor( client: UsingClient, interaction: APIMessageComponentSelectMenuInteraction, protected __reply?: __InternalReplyFunction, ) { super(client, interaction); const resolved = (interaction.data as APIMessageRoleSelectInteractionData).resolved; this.roles = this.values.map(x => Transformers.GuildRole(this.client, resolved.roles[x], this.guildId!)); } } export class UserSelectMenuInteraction extends SelectMenuInteraction { members: InteractionGuildMemberStructure[]; users: UserStructure[]; constructor( client: UsingClient, interaction: APIMessageComponentSelectMenuInteraction, protected __reply?: __InternalReplyFunction, ) { super(client, interaction); const resolved = (interaction.data as APIMessageUserSelectInteractionData).resolved; this.users = this.values.map(x => Transformers.User(this.client, resolved.users[x])); this.members = resolved.members ? this.values.map(x => Transformers.InteractionGuildMember( this.client, resolved.members![x], resolved.users[this.values!.find(u => u === x)!]!, this.guildId!, ), ) : []; } } export class ChatInputCommandInteraction extends ApplicationCommandInteraction< FromGuild, APIChatInputApplicationCommandInteraction > { declare data: ObjectToLower; } export class UserCommandInteraction extends ApplicationCommandInteraction< FromGuild, APIUserApplicationCommandInteraction > { declare type: ApplicationCommandType.User; declare data: ObjectToLower; } export class MessageCommandInteraction extends ApplicationCommandInteraction< FromGuild, APIMessageApplicationCommandInteraction > { declare type: ApplicationCommandType.Message; declare data: ObjectToLower; } export interface ModalSubmitInteraction extends Omit, 'modal'> {} @mix(Interaction) export class ModalSubmitInteraction extends BaseInteraction { declare data: ObjectToLower; get customId() { return this.data.customId; } get components() { return this.data.components; } getInputValue(customId: string, required: true): string; getInputValue(customId: string, required?: false): string | undefined; getInputValue(customId: string, required?: boolean): string | undefined { let value; for (const { components } of this.components) { const get = components.find(x => x.customId === customId); if (get) { value = get.value; break; } } if (!value && required) throw new Error(`${customId} component doesn't have a value`); return value; } }