diff --git a/mod.ts b/mod.ts index 8e72101..7262d9a 100644 --- a/mod.ts +++ b/mod.ts @@ -3,7 +3,6 @@ export * from "./structures/Attachment.ts"; export * from "./structures/Base.ts"; export * from "./structures/BaseGuild.ts"; export * from "./structures/BaseChannel.ts"; -export * from "./structures/Component.ts"; export * from "./structures/DMChannel.ts"; export * from "./structures/Embed.ts"; export * from "./structures/Emoji.ts"; @@ -25,6 +24,13 @@ export * from "./structures/VoiceChannel.ts"; export * from "./structures/WelcomeChannel.ts"; export * from "./structures/WelcomeScreen.ts"; +export * from "./structures/components/ActionRowComponent.ts"; +export * from "./structures/components/ButtonComponent.ts"; +export * from "./structures/components/Component.ts"; +export * from "./structures/components/LinkButtonComponent.ts"; +export * from "./structures/components/SelectMenuComponent.ts"; +export * from "./structures/components/TextInputComponent.ts"; + export * from "./session/Session.ts"; export * from "./util/shared/flags.ts"; diff --git a/structures/Component.ts b/structures/Component.ts deleted file mode 100644 index 02daff5..0000000 --- a/structures/Component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Session } from "../session/Session.ts"; -import type { DiscordComponent, MessageComponentTypes } from "../vendor/external.ts"; -import Emoji from "./Emoji.ts"; - -export class Component { - constructor(session: Session, data: DiscordComponent) { - this.session = session; - this.customId = data.custom_id; - this.type = data.type; - this.components = data.components?.map((component) => new Component(session, component)); - this.disabled = !!data.disabled; - - if (data.emoji) { - this.emoji = new Emoji(session, data.emoji); - } - - this.maxValues = data.max_values; - this.minValues = data.min_values; - this.label = data.label; - this.value = data.value; - this.options = data.options ?? []; - this.placeholder = data.placeholder; - } - - readonly session: Session; - - customId?: string; - type: MessageComponentTypes; - components?: Component[]; - disabled: boolean; - emoji?: Emoji; - maxValues?: number; - minValues?: number; - label?: string; - value?: string; - // deno-lint-ignore no-explicit-any - options: any[]; - placeholder?: string; -} - -export default Component; diff --git a/structures/Interaction.ts b/structures/Interaction.ts index f11dc99..820d6aa 100644 --- a/structures/Interaction.ts +++ b/structures/Interaction.ts @@ -38,6 +38,9 @@ export interface ApplicationCommandOptionChoice { value: string | number; } +// TODO: abstract Interaction, CommandInteraction, ComponentInteraction, PingInteraction, etc + + export class Interaction implements Model { constructor(session: Session, data: DiscordInteraction) { this.session = session; diff --git a/structures/Message.ts b/structures/Message.ts index 65f3c35..3d734d8 100644 --- a/structures/Message.ts +++ b/structures/Message.ts @@ -8,11 +8,13 @@ import type { DiscordUser, FileContent, } from "../vendor/external.ts"; +import type { Component } from "./components/Component.ts"; import type { GetReactions } from "../util/Routes.ts"; import { MessageFlags } from "../util/shared/flags.ts"; import User from "./User.ts"; import Member from "./Member.ts"; import Attachment from "./Attachment.ts"; +import BaseComponent from "./components/Component.ts"; import * as Routes from "../util/Routes.ts"; /** @@ -80,6 +82,8 @@ export class Message implements Model { if (data.guild_id && data.member) { this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id); } + + this.components = data.components?.map((component) => BaseComponent.from(session, component)); } readonly session: Session; @@ -95,6 +99,7 @@ export class Message implements Model { attachments: Attachment[]; member?: Member; + components?: Component[]; get url() { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; diff --git a/structures/components/ActionRowComponent.ts b/structures/components/ActionRowComponent.ts new file mode 100644 index 0000000..9976333 --- /dev/null +++ b/structures/components/ActionRowComponent.ts @@ -0,0 +1,39 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type { ActionRowComponent, Component } from "./Component.ts"; +import { MessageComponentTypes, ButtonStyles } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; +import Button from "./ButtonComponent.ts"; +import LinkButton from "./LinkButtonComponent.ts"; +import SelectMenu from "./SelectMenuComponent.ts"; +import InputText from "./TextInputComponent.ts"; + +export class ActionRow extends BaseComponent implements ActionRowComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.ActionRow; + this.components = data.components!.map((component) => { + switch (component.type) { + case MessageComponentTypes.Button: + if (component.style === ButtonStyles.Link) { + return new LinkButton(session, component); + } + return new Button(session, component); + case MessageComponentTypes.SelectMenu: + return new SelectMenu(session, component); + case MessageComponentTypes.InputText: + return new InputText(session, component); + case MessageComponentTypes.ActionRow: + throw new Error("Cannot have an action row inside an action row"); + } + }); + } + + readonly session: Session; + override type: MessageComponentTypes.ActionRow; + components: Array>; +} + +export default ActionRow; diff --git a/structures/components/ButtonComponent.ts b/structures/components/ButtonComponent.ts new file mode 100644 index 0000000..c0a47ab --- /dev/null +++ b/structures/components/ButtonComponent.ts @@ -0,0 +1,33 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent, ButtonStyles } from "../../vendor/external.ts"; +import type { ButtonComponent } from "./Component.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; +import Emoji from "../Emoji.ts"; + +export class Button extends BaseComponent implements ButtonComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.Button; + this.customId = data.custom_id; + this.label = data.label; + this.style = data.style as number; + this.disabled = data.disabled; + + if (data.emoji) { + this.emoji = new Emoji(session, data.emoji); + } + } + + readonly session: Session; + override type: MessageComponentTypes.Button; + customId?: string; + label?: string; + style: ButtonStyles.Primary | ButtonStyles.Secondary | ButtonStyles.Success | ButtonStyles.Danger; + disabled?: boolean; + emoji?: Emoji; +} + +export default Button; diff --git a/structures/components/Component.ts b/structures/components/Component.ts new file mode 100644 index 0000000..4bb2cf0 --- /dev/null +++ b/structures/components/Component.ts @@ -0,0 +1,122 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type Emoji from "../Emoji.ts"; +import { ButtonStyles, MessageComponentTypes, TextStyles } from "../../vendor/external.ts"; + +// TODO: fix circular dependencies +import ActionRow from "./ActionRowComponent.ts"; +import Button from "./ButtonComponent.ts"; +import LinkButton from "./ButtonComponent.ts"; +import SelectMenu from "./SelectMenuComponent.ts"; +import TextInput from "./TextInputComponent.ts"; + +export class BaseComponent { + constructor(type: MessageComponentTypes) { + this.type = type; + } + + type: MessageComponentTypes; + + isActionRow(): this is ActionRowComponent { + return this.type === MessageComponentTypes.ActionRow; + } + + isButton(): this is ButtonComponent { + return this.type === MessageComponentTypes.Button; + } + + isSelectMenu(): this is SelectMenuComponent { + return this.type === MessageComponentTypes.SelectMenu; + } + + isTextInput(): this is TextInputComponent { + return this.type === MessageComponentTypes.InputText; + } + + /** + * Component factory + * @internal + * */ + static from(session: Session, component: DiscordComponent): Component { + switch (component.type) { + case MessageComponentTypes.ActionRow: + return new ActionRow(session, component); + case MessageComponentTypes.Button: + if (component.style === ButtonStyles.Link) { + return new LinkButton(session, component); + } + return new Button(session, component); + case MessageComponentTypes.SelectMenu: + return new SelectMenu(session, component); + case MessageComponentTypes.InputText: + return new TextInput(session, component); + } + } +} + +/** Action Row Component */ +export interface ActionRowComponent { + type: MessageComponentTypes.ActionRow; + components: Array>; +} + +/** All Components */ +export type Component = + | ActionRowComponent + | ButtonComponent + | LinkButtonComponent + | SelectMenuComponent + | TextInputComponent; + +/** Button Component */ +export interface ButtonComponent { + type: MessageComponentTypes.Button; + style: ButtonStyles.Primary | ButtonStyles.Secondary | ButtonStyles.Success | ButtonStyles.Danger; + label?: string; + emoji?: Emoji; + customId?: string; + disabled?: boolean; +} + +/** Link Button Component */ +export interface LinkButtonComponent { + type: MessageComponentTypes.Button; + style: ButtonStyles.Link; + label?: string; + url: string; + disabled?: boolean; +} + +/** Select Menu Component */ +export interface SelectMenuComponent { + type: MessageComponentTypes.SelectMenu; + customId: string; + options: SelectMenuOption[]; + placeholder?: string; + minValue?: number; + maxValue?: number; + disabled?: boolean; +} + +/** Text Input Component */ +export interface TextInputComponent { + type: MessageComponentTypes.InputText; + customId: string; + style: TextStyles; + label: string; + minLength?: number; + maxLength?: number; + required?: boolean; + value?: string; + placeholder?: string; +} + +export interface SelectMenuOption { + label: string; + value: string; + description?: string; + emoji?: Emoji; + default?: boolean; +} + +export default BaseComponent; diff --git a/structures/components/LinkButtonComponent.ts b/structures/components/LinkButtonComponent.ts new file mode 100644 index 0000000..65b01cb --- /dev/null +++ b/structures/components/LinkButtonComponent.ts @@ -0,0 +1,33 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent, ButtonStyles } from "../../vendor/external.ts"; +import type { LinkButtonComponent } from "./Component.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; +import Emoji from "../Emoji.ts"; + +export class LinkButton extends BaseComponent implements LinkButtonComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.Button; + this.url = data.url!; + this.label = data.label; + this.style = data.style as number; + this.disabled = data.disabled; + + if (data.emoji) { + this.emoji = new Emoji(session, data.emoji); + } + } + + readonly session: Session; + override type: MessageComponentTypes.Button; + url: string; + label?: string; + style: ButtonStyles.Link; + disabled?: boolean; + emoji?: Emoji; +} + +export default LinkButton; diff --git a/structures/components/SelectMenuComponent.ts b/structures/components/SelectMenuComponent.ts new file mode 100644 index 0000000..40f61de --- /dev/null +++ b/structures/components/SelectMenuComponent.ts @@ -0,0 +1,39 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type { SelectMenuComponent, SelectMenuOption } from "./Component.ts"; +import { MessageComponentTypes } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; +import Emoji from "../Emoji.ts"; + +export class SelectMenu extends BaseComponent implements SelectMenuComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.SelectMenu; + this.customId = data.custom_id!; + this.options = data.options!.map((option) => { + return { + label: option.label, + description: option.description, + emoji: option.emoji || new Emoji(session, option.emoji!), + value: option.value, + }; + }); + this.placeholder = data.placeholder; + this.minValues = data.min_values; + this.maxValues = data.max_values; + this.disabled = data.disabled; + } + + readonly session: Session; + override type: MessageComponentTypes.SelectMenu; + customId: string; + options: SelectMenuOption[]; + placeholder?: string; + minValues?: number; + maxValues?: number; + disabled?: boolean; +} + +export default SelectMenu; diff --git a/structures/components/TextInputComponent.ts b/structures/components/TextInputComponent.ts new file mode 100644 index 0000000..27abfa3 --- /dev/null +++ b/structures/components/TextInputComponent.ts @@ -0,0 +1,38 @@ +import type { Session } from "../../session/Session.ts"; +import type { DiscordComponent } from "../../vendor/external.ts"; +import type { TextInputComponent } from "./Component.ts"; +import { MessageComponentTypes, TextStyles } from "../../vendor/external.ts"; +import BaseComponent from "./Component.ts"; + +export class TextInput extends BaseComponent implements TextInputComponent { + constructor(session: Session, data: DiscordComponent) { + super(data.type); + + this.session = session; + this.type = data.type as MessageComponentTypes.InputText; + this.customId = data.custom_id!; + this.label = data.label!; + this.style = data.style as TextStyles; + + this.placeholder = data.placeholder; + this.value = data.value; + + // @ts-ignore: vendor bug + this.minLength = data.min_length; + + // @ts-ignore: vendor bug + this.maxLength = data.max_length; + } + + readonly session: Session; + override type: MessageComponentTypes.InputText; + style: TextStyles; + customId: string; + label: string; + placeholder?: string; + value?: string; + minLength?: number; + maxLength?: number; +} + +export default TextInput;