diff --git a/:x b/:x new file mode 100644 index 0000000..43b5f29 --- /dev/null +++ b/:x @@ -0,0 +1,36 @@ + +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordInteraction } from "../../vendor/external.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import * as Routes from "../../util/Routes.ts"; + +export class PingInteraction extends BaseInteraction implements Model { + constructor(session: Session, data: DiscordInteraction) { + super(session, data); + this.commandId = data.data!.id; + this.commandName = data.data!.name; + this.commandType = data.data!.type; + this.commandGuildId = data.data!.guild_id; + } + + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + + async pong() { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.INTERACTION_ID_TOKEN(this.id, this.token), + { + type: InteractionResponseTypes.Pong, + } + ); + } +} + +export default PingInteraction; diff --git a/handlers/Actions.ts b/handlers/Actions.ts index e1dd524..371a37b 100644 --- a/handlers/Actions.ts +++ b/handlers/Actions.ts @@ -38,7 +38,7 @@ import Member from "../structures/Member.ts"; import Message from "../structures/Message.ts"; import User from "../structures/User.ts"; import Guild from "../structures/guilds/Guild.ts"; -import Interaction from "../structures/interactions/Interaction.ts"; +import InteractionFactory from "../structures/interactions/interactions/InteractionFactory.ts"; export type RawHandler = (...args: [Session, number, T]) => void; export type Handler = (...args: T) => unknown; @@ -106,12 +106,7 @@ export const GUILD_ROLE_DELETE: RawHandler = (session, _ } export const INTERACTION_CREATE: RawHandler = (session, _shardId, interaction) => { - session.unrepliedInteractions.add(BigInt(interaction.id)); - - // could be improved - setTimeout(() => session.unrepliedInteractions.delete(BigInt(interaction.id)), 15 * 60 * 1000); - - session.emit("interactionCreate", new Interaction(session, interaction)); + session.emit("interactionCreate", InteractionFactory.from(session, interaction)); }; export const CHANNEL_CREATE: RawHandler = (session, _shardId, channel) => { diff --git a/mod.ts b/mod.ts index ab987f5..f43a323 100644 --- a/mod.ts +++ b/mod.ts @@ -37,7 +37,14 @@ export * from "./structures/guilds/InviteGuild.ts"; export * from "./structures/builders/EmbedBuilder.ts"; -export * from "./structures/interactions/Interaction.ts"; +export * from "./structures/interactions/AutoCompleteInteraction.ts"; +export * from "./structures/interactions/BaseInteraction.ts"; +export * from "./structures/interactions/CommandInteraction.ts"; +export * from "./structures/interactions/CommandInteractionOptionResolver.ts"; +export * from "./structures/interactions/ComponentInteraction.ts"; +export * from "./structures/interactions/InteractionFactory.ts"; +export * from "./structures/interactions/ModalSubmitInteraction.ts"; +export * from "./structures/interactions/PingInteraction.ts"; export * from "./session/Session.ts"; diff --git a/session/Session.ts b/session/Session.ts index 6b25993..50b9e61 100644 --- a/session/Session.ts +++ b/session/Session.ts @@ -39,8 +39,6 @@ export class Session extends EventEmitter { rest: ReturnType; gateway: ReturnType; - unrepliedInteractions: Set = new Set(); - #botId: Snowflake; #applicationId?: Snowflake; diff --git a/structures/interactions/AutoCompleteInteraction.ts b/structures/interactions/AutoCompleteInteraction.ts new file mode 100644 index 0000000..877d87e --- /dev/null +++ b/structures/interactions/AutoCompleteInteraction.ts @@ -0,0 +1,40 @@ + +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import type { ApplicationCommandOptionChoice } from "./BaseInteraction.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import * as Routes from "../../util/Routes.ts"; + +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; + } + + override type: InteractionTypes.ApplicationCommandAutocomplete; + commandId: Snowflake; + commandName: string; + commandType: ApplicationCommandTypes; + commandGuildId?: Snowflake; + + async respond(choices: ApplicationCommandOptionChoice[]) { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.INTERACTION_ID_TOKEN(this.id, this.token), + { + data: { choices }, + type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, + } + ); + } +} + +export default AutoCompleteInteraction; diff --git a/structures/interactions/BaseInteraction.ts b/structures/interactions/BaseInteraction.ts new file mode 100644 index 0000000..a93ca8f --- /dev/null +++ b/structures/interactions/BaseInteraction.ts @@ -0,0 +1,84 @@ +import type { Model } from "../Base.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction } from "../../vendor/external.ts"; +import type CommandInteraction from "./CommandInteraction.ts"; +import type PingInteraction from "./PingInteraction.ts"; +import { InteractionTypes } from "../../vendor/external.ts"; +import { Snowflake } from "../../util/Snowflake.ts"; +import User from "../User.ts"; +import Member from "../Member.ts"; +import Permsisions from "../Permissions.ts"; + +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; + + // @ts-expect-error: vendor error + const perms = data.app_permissions as string; + + if (perms) { + this.appPermissions = new Permsisions(BigInt(perms)); + } + + if (!data.guild_id) { + this.user = new User(session, data.user!); + } else { + this.member = new Member(session, data.member!, data.guild_id); + } + } + + readonly session: Session; + readonly id: Snowflake; + readonly token: string; + + type: InteractionTypes; + guildId?: Snowflake; + channelId?: Snowflake; + applicationId?: Snowflake; + user?: User; + member?: Member; + appPermissions?: Permsisions; + + readonly version: 1; + + get createdTimestamp() { + return Snowflake.snowflakeToTimestamp(this.id); + } + + get createdAt() { + return new Date(this.createdTimestamp); + } + + isCommand(): this is CommandInteraction { + return this.type === InteractionTypes.ApplicationCommand; + } + + isAutoComplete() { + return this.type === InteractionTypes.ApplicationCommandAutocomplete; + } + + isComponent() { + return this.type === InteractionTypes.MessageComponent; + } + + isPing(): this is PingInteraction { + return this.type === InteractionTypes.Ping; + } + + isModalSubmit() { + return this.type === InteractionTypes.ModalSubmit; + } + + inGuild() { + return !!this.guildId; + } +} + +export default BaseInteraction; diff --git a/structures/interactions/CommandInteraction.ts b/structures/interactions/CommandInteraction.ts new file mode 100644 index 0000000..ef1294a --- /dev/null +++ b/structures/interactions/CommandInteraction.ts @@ -0,0 +1,108 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordMemberWithUser, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import type { CreateMessage } from "../Message.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import CommandInteractionOptionResolver from "./CommandInteractionOptionResolver.ts"; +import Attachment from "../Attachment.ts"; +import User from "../User.ts"; +import Member from "../Member.ts"; +import Message from "../Message.ts"; +import Role from "../Role.ts"; + +/** + * @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 Omit { + customId?: string; + title?: string; + // TODO: use builder + // components?: MessageComponents; + flags?: number; + choices?: ApplicationCommandOptionChoice[]; +} + +/** + * @link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice + * */ +export interface ApplicationCommandOptionChoice { + name: string; + value: string | number; +} + +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 CommandInteractionOptionResolver(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: { + users: Map; + members: Map; + roles: Map; + attachments: Map; + messages: Map; + }; + options: CommandInteractionOptionResolver; + responded = false; +} + +export default CommandInteraction; diff --git a/structures/interactions/CommandInteractionOptionResolver.ts b/structures/interactions/CommandInteractionOptionResolver.ts new file mode 100644 index 0000000..a654566 --- /dev/null +++ b/structures/interactions/CommandInteractionOptionResolver.ts @@ -0,0 +1,233 @@ +import type { DiscordInteractionDataOption, DiscordInteractionDataResolved } from '../../vendor/external.ts'; +import { ApplicationCommandOptionTypes } from "../../vendor/external.ts"; + +export function transformOasisInteractionDataOption(o: DiscordInteractionDataOption): CommandInteractionOption { + const output: CommandInteractionOption = { ...o, Otherwise: o.value as string | boolean | number | undefined }; + + switch (o.type) { + case ApplicationCommandOptionTypes.String: + output.String = o.value as string; + break; + case ApplicationCommandOptionTypes.Number: + output.Number = o.value as number; + break; + case ApplicationCommandOptionTypes.Integer: + output.Integer = o.value as number; + break; + case ApplicationCommandOptionTypes.Boolean: + output.Boolean = o.value as boolean; + break; + case ApplicationCommandOptionTypes.Role: + output.Role = BigInt(o.value as string); + break; + case ApplicationCommandOptionTypes.User: + output.User = BigInt(o.value as string); + break; + case ApplicationCommandOptionTypes.Channel: + output.Channel = BigInt(o.value as string); + break; + + case ApplicationCommandOptionTypes.Mentionable: + case ApplicationCommandOptionTypes.SubCommand: + case ApplicationCommandOptionTypes.SubCommandGroup: + default: + output.Otherwise = o.value as string | boolean | number | undefined; + } + + return output; +} + +export interface CommandInteractionOption extends Omit { + Attachment?: string; + Boolean?: boolean; + User?: bigint; + Role?: bigint; + Number?: number; + Integer?: number; + Channel?: bigint; + String?: string; + Mentionable?: string; + Otherwise: string | number | boolean | bigint | undefined; +} + +/** + * Utility class to get the resolved options for a command + * It is really typesafe + * @example const option = ctx.options.getStringOption("name"); + */ +export class CommandInteractionOptionResolver { + #subcommand?: string; + #group?: string; + + hoistedOptions: CommandInteractionOption[]; + resolved?: DiscordInteractionDataResolved; + + constructor(options?: DiscordInteractionDataOption[], resolved?: DiscordInteractionDataResolved) { + this.hoistedOptions = options?.map(transformOasisInteractionDataOption) ?? []; + + // warning: black magic do not edit and thank djs authors + + if (this.hoistedOptions[0]?.type === ApplicationCommandOptionTypes.SubCommandGroup) { + this.#group = this.hoistedOptions[0].name; + this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(transformOasisInteractionDataOption); + } + + if (this.hoistedOptions[0]?.type === ApplicationCommandOptionTypes.SubCommand) { + this.#subcommand = this.hoistedOptions[0].name; + this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(transformOasisInteractionDataOption); + } + + this.resolved = resolved; + } + + private getTypedOption( + name: string | number, + type: ApplicationCommandOptionTypes, + properties: Array, + required: boolean, + ) { + const option = this.get(name, required); + + if (!option) { + return; + } + + if (option.type !== type) { + // pass + } + + if (required === true && properties.every((prop) => typeof option[prop] === "undefined")) { + throw new TypeError(`Properties ${properties.join(', ')} are missing in option ${name}`); + } + + return option; + } + + get(name: string | number, required: true): CommandInteractionOption; + get(name: string | number, required: boolean): CommandInteractionOption | undefined; + get(name: string | number, required?: boolean) { + const option = this.hoistedOptions.find((o) => + typeof name === 'number' ? o.name === name.toString() : o.name === name + ); + + if (!option) { + if (required && name in this.hoistedOptions.map((o) => o.name)) { + throw new TypeError('Option marked as required was undefined'); + } + + return; + } + + return option; + } + + /** searches for a string option */ + getString(name: string | number, required: true): string; + getString(name: string | number, required?: boolean): string | undefined; + getString(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.String, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a number option */ + getNumber(name: string | number, required: true): number; + getNumber(name: string | number, required?: boolean): number | undefined; + getNumber(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Number, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searhces for an integer option */ + getInteger(name: string | number, required: true): number; + getInteger(name: string | number, required?: boolean): number | undefined; + getInteger(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Integer, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a boolean option */ + getBoolean(name: string | number, required: true): boolean; + getBoolean(name: string | number, required?: boolean): boolean | undefined; + getBoolean(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Boolean, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a user option */ + getUser(name: string | number, required: true): bigint; + getUser(name: string | number, required?: boolean): bigint | undefined; + getUser(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.User, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a channel option */ + getChannel(name: string | number, required: true): bigint; + getChannel(name: string | number, required?: boolean): bigint | undefined; + getChannel(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Channel, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a mentionable-based option */ + getMentionable(name: string | number, required: true): string; + getMentionable(name: string | number, required?: boolean): string | undefined; + getMentionable(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Mentionable, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for a mentionable-based option */ + getRole(name: string | number, required: true): bigint; + getRole(name: string | number, required?: boolean): bigint | undefined; + getRole(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Role, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for an attachment option */ + getAttachment(name: string | number, required: true): string; + getAttachment(name: string | number, required?: boolean): string | undefined; + getAttachment(name: string | number, required = false) { + const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Attachment, ['Otherwise'], required); + + return option?.Otherwise ?? undefined; + } + + /** searches for the focused option */ + getFocused(full = false) { + const focusedOption = this.hoistedOptions.find((option) => option.focused); + + if (!focusedOption) { + throw new TypeError('No option found'); + } + + return full ? focusedOption : focusedOption.Otherwise; + } + + getSubCommand(required = true) { + if (required && !this.#subcommand) { + throw new TypeError('Option marked as required was undefined'); + } + + return [this.#subcommand, this.hoistedOptions]; + } + + getSubCommandGroup(required = false) { + if (required && !this.#group) { + throw new TypeError('Option marked as required was undefined'); + } + + return [this.#group, this.hoistedOptions]; + } +} + +export default CommandInteractionOptionResolver; diff --git a/structures/interactions/ComponentInteraction.ts b/structures/interactions/ComponentInteraction.ts new file mode 100644 index 0000000..a85524f --- /dev/null +++ b/structures/interactions/ComponentInteraction.ts @@ -0,0 +1,45 @@ +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import Message from "../Message.ts"; + +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; + responded = false; + + isButton() { + return this.componentType === MessageComponentTypes.Button; + } + + isActionRow() { + return this.componentType === MessageComponentTypes.ActionRow; + } + + isTextInput() { + return this.componentType === MessageComponentTypes.InputText; + } + + isSelectMenu() { + return this.componentType === MessageComponentTypes.SelectMenu; + } +} + +export default ComponentInteraction; diff --git a/structures/interactions/Interaction.ts b/structures/interactions/Interaction.ts deleted file mode 100644 index 5ed2b94..0000000 --- a/structures/interactions/Interaction.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Model } from "../Base.ts"; -import type { Snowflake } from "../../util/Snowflake.ts"; -import type { Session } from "../../session/Session.ts"; -import type { - DiscordInteraction, - DiscordMessage, - FileContent, - InteractionResponseTypes, - InteractionTypes, -} from "../../vendor/external.ts"; -import type { MessageFlags } from "../../util/shared/flags.ts"; -import type { AllowedMentions } from "../Message.ts"; -import User from "../User.ts"; -import Message from "../Message.ts"; -import Member from "../Member.ts"; -import * as Routes from "../../util/Routes.ts"; - -export interface InteractionResponse { - type: InteractionResponseTypes; - data?: InteractionApplicationCommandCallbackData; -} - -export interface InteractionApplicationCommandCallbackData { - content?: string; - tts?: boolean; - allowedMentions?: AllowedMentions; - files?: FileContent[]; - customId?: string; - title?: string; - // components?: Component[]; - flags?: MessageFlags; - choices?: ApplicationCommandOptionChoice[]; -} - -/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */ -export interface ApplicationCommandOptionChoice { - name: string; - value: string | number; -} - -// TODO: abstract Interaction, CommandInteraction, ComponentInteraction, PingInteraction, etc - -export class Interaction 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.locale = data.locale; - this.data = data.data; - - if (!data.guild_id) { - this.user = new User(session, data.user!); - } else { - this.member = new Member(session, data.member!, data.guild_id); - } - } - - readonly session: Session; - readonly id: Snowflake; - readonly token: string; - - type: InteractionTypes; - guildId?: Snowflake; - channelId?: Snowflake; - applicationId?: Snowflake; - locale?: string; - // deno-lint-ignore no-explicit-any - data: any; - user?: User; - member?: Member; - - async respond({ type, data }: InteractionResponse) { - const toSend = { - tts: data?.tts, - title: data?.title, - flags: data?.flags, - content: data?.content, - choices: data?.choices, - custom_id: data?.customId, - allowed_mentions: data?.allowedMentions - ? { - users: data.allowedMentions.users, - roles: data.allowedMentions.roles, - parse: data.allowedMentions.parse, - replied_user: data.allowedMentions.repliedUser, - } - : { parse: [] }, - }; - - if (this.session.unrepliedInteractions.delete(BigInt(this.id))) { - await this.session.rest.sendRequest( - this.session.rest, - { - url: Routes.INTERACTION_ID_TOKEN(this.id, this.token), - method: "POST", - payload: this.session.rest.createRequestBody(this.session.rest, { - method: "POST", - body: { - type: type, - data: toSend, - file: data?.files, - }, - headers: { - // remove authorization header - Authorization: "", - }, - }), - }, - ); - - return; - } - - const result = await this.session.rest.sendRequest( - this.session.rest, - { - url: Routes.WEBHOOK(this.session.applicationId ?? this.session.botId, this.token), - method: "POST", - payload: this.session.rest.createRequestBody(this.session.rest, { - method: "POST", - body: { - ...toSend, - file: data?.files, - }, - headers: { - // remove authorization header - Authorization: "", - }, - }), - }, - ); - - return new Message(this.session, result); - } - - inGuild(): this is Interaction & { user: undefined; guildId: Snowflake; member: Member } { - return !!this.guildId; - } -} - -export default Interaction; diff --git a/structures/interactions/InteractionFactory.ts b/structures/interactions/InteractionFactory.ts new file mode 100644 index 0000000..1ed1f5a --- /dev/null +++ b/structures/interactions/InteractionFactory.ts @@ -0,0 +1,32 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction } from "../../vendor/external.ts"; +import { InteractionTypes } from "../../vendor/external.ts"; +import CommandInteraction from "./CommandInteraction.ts"; +import ComponentInteraction from "./ComponentInteraction.ts"; +import PingInteraction from "./PingInteraction.ts"; +import AutoCompleteInteraction from "./AutoCompleteInteraction.ts"; +import ModalSubmitInteraction from "./ModalSubmitInteraction.ts"; + +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); + } + } +} diff --git a/structures/interactions/ModalSubmitInteraction.ts b/structures/interactions/ModalSubmitInteraction.ts new file mode 100644 index 0000000..ff14a18 --- /dev/null +++ b/structures/interactions/ModalSubmitInteraction.ts @@ -0,0 +1,50 @@ + +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { DiscordInteraction, InteractionTypes, MessageComponentTypes, DiscordMessageComponents } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import Message from "../Message.ts"; + +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 ModalSubmitInteraction & { message: Message } { + return !!this.message; + } +} + +export default ModalSubmitInteraction; diff --git a/structures/interactions/PingInteraction.ts b/structures/interactions/PingInteraction.ts new file mode 100644 index 0000000..438ac01 --- /dev/null +++ b/structures/interactions/PingInteraction.ts @@ -0,0 +1,38 @@ + +import type { Model } from "../Base.ts"; +import type { Snowflake } from "../../util/Snowflake.ts"; +import type { Session } from "../../session/Session.ts"; +import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts"; +import { InteractionResponseTypes } from "../../vendor/external.ts"; +import BaseInteraction from "./BaseInteraction.ts"; +import * as Routes from "../../util/Routes.ts"; + +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; + + async pong() { + await this.session.rest.runMethod( + this.session.rest, + "POST", + Routes.INTERACTION_ID_TOKEN(this.id, this.token), + { + type: InteractionResponseTypes.Pong, + } + ); + } +} + +export default PingInteraction;