import type { Model } from './base'; import type { Session } from '../biscuit'; import type { ApplicationCommandTypes, DiscordInteraction, DiscordMessage, DiscordMessageComponents, DiscordMemberWithUser, DiscordMessageInteraction, Locales } from '@biscuitland/api-types'; import type { CreateMessage } from './message'; import type { MessageFlags } from '../utils/util'; import type { EditWebhookMessage } from './webhook'; import { InteractionResponseTypes, InteractionTypes, MessageComponentTypes, INTERACTION_ID_TOKEN, WEBHOOK_MESSAGE, WEBHOOK_MESSAGE_ORIGINAL } from '@biscuitland/api-types'; import { Role } from './role'; import { Attachment } from './attachment'; import { Snowflake } from '../snowflakes'; import { User } from './user'; import { Member } from './members'; import { Message } from './message'; import { Permissions } from './special/permissions'; import { Webhook } from './webhook'; import { InteractionOptions } from './special/interaction-options'; export type InteractionResponseWith = { with: InteractionApplicationCommandCallbackData; }; export type InteractionResponseWithData = | InteractionResponse | InteractionResponseWith; /** * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response */ export interface InteractionResponse { type: InteractionResponseTypes; data?: InteractionApplicationCommandCallbackData; } /** * @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata */ export interface InteractionApplicationCommandCallbackData extends Pick< CreateMessage, 'allowedMentions' | 'content' | 'embeds' | 'files' > { customId?: string; title?: string; components?: DiscordMessageComponents; flags?: MessageFlags; choices?: ApplicationCommandOptionChoice[]; } /** * @link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */ export interface ApplicationCommandOptionChoice { name: string; value: string | number; } export abstract class BaseInteraction implements Model { constructor(session: Session, data: DiscordInteraction) { this.session = session; this.id = data.id; this.token = data.token; this.type = data.type; this.guildId = data.guild_id; this.channelId = data.channel_id; this.applicationId = data.application_id; this.version = data.version; this.locale = data.locale as Locales; const perms = data.app_permissions; if (perms) { this.appPermissions = new Permissions(BigInt(perms)); } if (!data.guild_id) { this.user = new User(session, data.user!); } else { this.member = new Member(session, data.member!, data.guild_id); // dangerous black magic be careful! Object.defineProperty(this, 'user', { get() { return this.member.user; } }); } } readonly session: Session; readonly id: Snowflake; readonly token: string; type: InteractionTypes; guildId?: Snowflake; channelId?: Snowflake; applicationId?: Snowflake; user!: User; member?: Member; appPermissions?: Permissions; // must be implemented locale: Locales; // readonly property according to docs readonly version: 1; responded = false; get createdTimestamp(): number { return Snowflake.snowflakeToTimestamp(this.id); } get createdAt(): Date { return new Date(this.createdTimestamp); } isCommand(): this is CommandInteraction { return this.type === InteractionTypes.ApplicationCommand; } isAutoComplete(): this is AutoCompleteInteraction { return this.type === InteractionTypes.ApplicationCommandAutocomplete; } isComponent(): this is ComponentInteraction { return this.type === InteractionTypes.MessageComponent; } isPing(): this is PingInteraction { return this.type === InteractionTypes.Ping; } isModalSubmit(): this is ModalSubmitInteraction { return this.type === InteractionTypes.ModalSubmit; } inGuild(): this is this & { guildId: Snowflake } { return !!this.guildId; } // webhooks methods: async editReply( options: EditWebhookMessage & { messageId?: Snowflake } ): Promise { const message = await this.session.rest.patch< DiscordMessage | undefined >( options.messageId ? WEBHOOK_MESSAGE(this.session.applicationId, this.token, options.messageId) : WEBHOOK_MESSAGE_ORIGINAL(this.session.applicationId, this.token), { content: options.content, embeds: options.embeds, file: options.files, components: options.components, allowed_mentions: options.allowedMentions && { parse: options.allowedMentions.parse, replied_user: options.allowedMentions.repliedUser, users: options.allowedMentions.users, roles: options.allowedMentions.roles, }, attachments: options.attachments?.map(attachment => { return { id: attachment.id, filename: attachment.name, content_type: attachment.contentType, size: attachment.size, url: attachment.attachment, proxy_url: attachment.proxyUrl, height: attachment.height, width: attachment.width, }; }), message_id: options.messageId, } ); if (!message || !options.messageId) { return message as undefined; } return new Message(this.session, message); } async sendFollowUp( options: InteractionApplicationCommandCallbackData ): Promise { const message = await Webhook.prototype.execute.call( { id: this.applicationId!, token: this.token, session: this.session, }, options ); return message!; } async editFollowUp( messageId: Snowflake, options?: { threadId: Snowflake } ): Promise { const message = await Webhook.prototype.editMessage.call( { id: this.session.applicationId, session: this.session, token: this.token, }, messageId, options ); return message; } async deleteFollowUp( messageId: Snowflake, threadId?: Snowflake ): Promise { await Webhook.prototype.deleteMessage.call( { id: this.session.applicationId, session: this.session, token: this.token, }, messageId, threadId ); } async fetchFollowUp( messageId: Snowflake, threadId?: Snowflake ): Promise { const message = await Webhook.prototype.fetchMessage.call( { id: this.session.applicationId, session: this.session, token: this.token, }, messageId, threadId ); return message; } // end webhook methods async respond(resp: InteractionResponse): Promise; async respond(resp: InteractionResponseWith): Promise; async respond( resp: InteractionResponseWithData ): Promise { const options = 'with' in resp ? resp.with : resp.data; const type = 'type' in resp ? resp.type : InteractionResponseTypes.ChannelMessageWithSource; const data = { content: options?.content, custom_id: options?.customId, file: options?.files, allowed_mentions: options?.allowedMentions, flags: options?.flags, chocies: options?.choices, embeds: options?.embeds, title: options?.title, components: options?.components, }; if (!this.responded) { await this.session.rest.post( INTERACTION_ID_TOKEN(this.id, this.token), { file: options?.files, type, data, } ); this.responded = true; return; } return this.sendFollowUp(data); } // start custom methods async respondWith( resp: InteractionApplicationCommandCallbackData ): Promise { const m = await this.respond({ with: resp }); return m; } session: this.session, async defer() { await this.respond({ type: InteractionResponseTypes.DeferredChannelMessageWithSource, }); } async autocomplete() { await this.respond({ type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, }); } // end custom methods } export class AutoCompleteInteraction extends BaseInteraction implements Model { constructor(session: Session, data: DiscordInteraction) { super(session, data); this.type = data.type as number; this.commandId = data.data!.id; this.commandName = data.data!.name; this.commandType = data.data!.type; this.commandGuildId = data.data!.guild_id; this.options = new InteractionOptions( data.data!.options ?? [] ); } override type: InteractionTypes.ApplicationCommandAutocomplete; commandId: Snowflake; commandName: string; commandType: ApplicationCommandTypes; commandGuildId?: Snowflake; options: InteractionOptions; async respondWithChoices( choices: ApplicationCommandOptionChoice[] ): Promise { await this.session.rest.post( INTERACTION_ID_TOKEN(this.id, this.token), { data: { choices }, type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, } ); } } export interface CommandInteractionDataResolved { users: Map; members: Map; roles: Map; messages: Map; attachments: Map; } export class CommandInteraction extends BaseInteraction implements Model { constructor(session: Session, data: DiscordInteraction) { super(session, data); this.type = data.type as number; this.commandId = data.data!.id; this.commandName = data.data!.name; this.commandType = data.data!.type; this.commandGuildId = data.data!.guild_id; this.options = new InteractionOptions( data.data!.options ?? [] ); this.resolved = { users: new Map(), members: new Map(), roles: new Map(), attachments: new Map(), messages: new Map(), }; if (data.data!.resolved?.users) { for (const [id, u] of Object.entries(data.data!.resolved.users)) { this.resolved.users.set(id, new User(session, u)); } } if (data.data!.resolved?.members && !!super.guildId) { for (const [id, m] of Object.entries(data.data!.resolved.members)) { this.resolved.members.set( id, new Member( session, m as DiscordMemberWithUser, super.guildId! ) ); } } if (data.data!.resolved?.roles && !!super.guildId) { for (const [id, r] of Object.entries(data.data!.resolved.roles)) { this.resolved.roles.set( id, new Role(session, r, super.guildId!) ); } } if (data.data!.resolved?.attachments) { for (const [id, a] of Object.entries( data.data!.resolved.attachments )) { this.resolved.attachments.set(id, new Attachment(session, a)); } } if (data.data!.resolved?.messages) { for (const [id, m] of Object.entries( data.data!.resolved.messages )) { this.resolved.messages.set(id, new Message(session, m)); } } } override type: InteractionTypes.ApplicationCommand; commandId: Snowflake; commandName: string; commandType: ApplicationCommandTypes; commandGuildId?: Snowflake; resolved: CommandInteractionDataResolved; options: InteractionOptions; } export type ModalInMessage = ModalSubmitInteraction & { message: Message; }; export class ModalSubmitInteraction extends BaseInteraction implements Model { constructor(session: Session, data: DiscordInteraction) { super(session, data); this.type = data.type as number; this.componentType = data.data!.component_type!; this.customId = data.data!.custom_id; this.targetId = data.data!.target_id; this.values = data.data!.values; this.components = data.data?.components?.map( ModalSubmitInteraction.transformComponent ); if (data.message) { this.message = new Message(session, data.message); } } override type: InteractionTypes.MessageComponent; componentType: MessageComponentTypes; customId?: string; targetId?: Snowflake; values?: string[]; message?: Message; components; static transformComponent(component: DiscordMessageComponents[number]) { return { type: component.type, components: component.components.map(component => { return { customId: component.custom_id, value: (component as typeof component & { value: string }) .value, }; }), }; } inMessage(): this is ModalInMessage { return !!this.message; } } export class PingInteraction extends BaseInteraction implements Model { constructor(session: Session, data: DiscordInteraction) { super(session, data); this.type = data.type as number; this.commandId = data.data!.id; this.commandName = data.data!.name; this.commandType = data.data!.type; this.commandGuildId = data.data!.guild_id; } override type: InteractionTypes.Ping; commandId: Snowflake; commandName: string; commandType: ApplicationCommandTypes; commandGuildId?: Snowflake; override locale = undefined as never; async pong(): Promise { await this.session.rest.post( INTERACTION_ID_TOKEN(this.id, this.token), { type: InteractionResponseTypes.Pong, } ); } } export class ComponentInteraction extends BaseInteraction implements Model { constructor(session: Session, data: DiscordInteraction) { super(session, data); this.type = data.type as number; this.componentType = data.data!.component_type!; this.customId = data.data!.custom_id; this.targetId = data.data!.target_id; this.values = data.data!.values; this.message = new Message(session, data.message!); } override type: InteractionTypes.MessageComponent; componentType: MessageComponentTypes; customId?: string; targetId?: Snowflake; values?: string[]; message: Message; isButton(): boolean { return this.componentType === MessageComponentTypes.Button; } isActionRow(): boolean { return this.componentType === MessageComponentTypes.ActionRow; } isTextInput(): boolean { return this.componentType === MessageComponentTypes.InputText; } isSelectMenu(): boolean { return this.componentType === MessageComponentTypes.SelectMenu; } async deferUpdate() { await this.respond({ type: InteractionResponseTypes.DeferredUpdateMessage, }); } } /** * @link https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object-message-interaction-structure */ export interface MessageInteraction { /** id of the interaction */ id: Snowflake; /** type of interaction */ type: InteractionTypes; /** name of the application command, including subcommands and subcommand groups */ name: string; /** user who invoked the interaction */ user: User; /** member who invoked the interaction in the guild */ member?: Partial; } export type Interaction = | CommandInteraction | ComponentInteraction | PingInteraction | AutoCompleteInteraction | ModalSubmitInteraction; export class InteractionFactory { static from( session: Session, interaction: DiscordInteraction ): Interaction { switch (interaction.type) { case InteractionTypes.Ping: return new PingInteraction(session, interaction); case InteractionTypes.ApplicationCommand: return new CommandInteraction(session, interaction); case InteractionTypes.MessageComponent: return new ComponentInteraction(session, interaction); case InteractionTypes.ApplicationCommandAutocomplete: return new AutoCompleteInteraction(session, interaction); case InteractionTypes.ModalSubmit: return new ModalSubmitInteraction(session, interaction); } } static fromMessage( session: Session, interaction: DiscordMessageInteraction, _guildId?: Snowflake ): MessageInteraction { const obj = { id: interaction.id, type: interaction.type, name: interaction.name, user: new User(session, interaction.user), // TODO: Parse member somehow with the guild id passed in message }; return obj; } }