diff --git a/src/helpers/mod.ts b/src/helpers/mod.ts index 5d928852c..639177c4e 100644 --- a/src/helpers/mod.ts +++ b/src/helpers/mod.ts @@ -114,8 +114,9 @@ import { getGuildTemplates } from "./templates/get_guild_templates.ts"; import { getTemplate } from "./templates/get_template.ts"; import { syncGuildTemplate } from "./templates/sync_guild_template.ts"; // Type Guards -import { isActionRow } from "./type_guards/is_action_row.ts"; import { isButton } from "./type_guards/is_button.ts"; +import { isSelectMenu } from "./type_guards/is_select_menu.ts"; + import { createWebhook } from "./webhooks/create_webhook.ts"; import { deleteWebhook } from "./webhooks/delete_webhook.ts"; import { deleteWebhookMessage } from "./webhooks/delete_webhook_message.ts"; @@ -239,8 +240,8 @@ export { guildBannerURL, guildIconURL, guildSplashURL, - isActionRow, isButton, + isSelectMenu, isChannelSynced, kick, kickMember, diff --git a/src/helpers/type_guards/is_action_row.ts b/src/helpers/type_guards/is_action_row.ts deleted file mode 100644 index 145f89a60..000000000 --- a/src/helpers/type_guards/is_action_row.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ActionRow } from "../../types/messages/components/action_row.ts"; -import type { MessageComponent } from "../../types/messages/components/message_components.ts"; -import { MessageComponentTypes } from "../../types/messages/components/message_component_types.ts"; - -/** A type guard function to tell if it is a action row component */ -export function isActionRow(component: MessageComponent): component is ActionRow { - return component.type === MessageComponentTypes.ActionRow; -} diff --git a/src/helpers/type_guards/is_button.ts b/src/helpers/type_guards/is_button.ts index 89483cd94..236d28fe9 100644 --- a/src/helpers/type_guards/is_button.ts +++ b/src/helpers/type_guards/is_button.ts @@ -1,8 +1,7 @@ import type { ButtonComponent } from "../../types/messages/components/button_component.ts"; -import type { MessageComponent } from "../../types/messages/components/message_components.ts"; -import { MessageComponentTypes } from "../../types/messages/components/message_component_types.ts"; +import type { ActionRoleComponents } from "../../types/messages/components/message_components.ts"; /** A type guard function to tell if it is a button component */ -export function isButton(component: MessageComponent): component is ButtonComponent { - return component.type === MessageComponentTypes.Button; +export function isButton(component: ActionRoleComponents): component is ButtonComponent { + return Reflect.has(component, "type"); } diff --git a/src/helpers/type_guards/is_select_menu.ts b/src/helpers/type_guards/is_select_menu.ts new file mode 100644 index 000000000..1e77f5319 --- /dev/null +++ b/src/helpers/type_guards/is_select_menu.ts @@ -0,0 +1,7 @@ +import type { ActionRoleComponents } from "../../types/messages/components/message_components.ts"; +import { SelectMenuComponent } from "../../types/messages/components/select_menu.ts"; + +/** A type guard function to tell if it is a button component */ +export function isSelectMenu(component: ActionRoleComponents): component is SelectMenuComponent { + return !Reflect.has(component, "type"); +} diff --git a/src/types/interactions/commands/application_command_interaction_data.ts b/src/types/interactions/commands/application_command_interaction_data.ts index e209cd208..1be81169a 100644 --- a/src/types/interactions/commands/application_command_interaction_data.ts +++ b/src/types/interactions/commands/application_command_interaction_data.ts @@ -11,8 +11,4 @@ export interface ApplicationCommandInteractionData { resolved?: ApplicationCommandInteractionDataResolved; /** The params + values from the user */ options?: ApplicationCommandInteractionDataOption[]; - /** with the value you defined for this component */ - customId?: string; - /** The type of this component */ - componentType?: 2; } diff --git a/src/types/interactions/interaction.ts b/src/types/interactions/interaction.ts index 09dc367cc..4d2181981 100644 --- a/src/types/interactions/interaction.ts +++ b/src/types/interactions/interaction.ts @@ -3,17 +3,32 @@ import { User } from "../users/user.ts"; import { ApplicationCommandInteractionData } from "./commands/application_command_interaction_data.ts"; import { InteractionGuildMember } from "./interaction_guild_member.ts"; import { DiscordInteractionTypes } from "./interaction_types.ts"; +import { SelectMenuData } from "../messages/components/select_data.ts"; +import { ButtonData } from "../messages/components/button_data.ts"; /** https://discord.com/developers/docs/interactions/slash-commands#interaction */ export interface Interaction { + /** The command data payload */ + data?: ApplicationCommandInteractionData | ButtonData | SelectMenuData; +} + +export interface SlashCommandInteraction extends BaseInteraction { + type: DiscordInteractionTypes.ApplicationCommand; + data?: ApplicationCommandInteractionData; +} + +export interface ComponentInteraction extends BaseInteraction { + type: DiscordInteractionTypes.MessageComponent; + data?: ButtonData | SelectMenuData; +} + +export interface BaseInteraction { /** Id of the interaction */ id: string; /** Id of the application this interaction is for */ applicationId: string; /** The type of interaction */ type: DiscordInteractionTypes; - /** The command data payload */ - data?: ApplicationCommandInteractionData; /** The guild it was sent from */ guildId?: string; /** The channel it was sent from */ diff --git a/src/types/messages/components/action_row.ts b/src/types/messages/components/action_row.ts index 27a4a26e2..09e9f2a8d 100644 --- a/src/types/messages/components/action_row.ts +++ b/src/types/messages/components/action_row.ts @@ -1,9 +1,15 @@ import { ButtonComponent } from "./button_component.ts"; +import { SelectMenuComponent } from "./select_menu.ts"; // TODO: add docs link export interface ActionRow { /** Action rows are a group of buttons. */ type: 1; - /** The button components */ - components: ButtonComponent[]; + /** The components in this row */ + components: + | [SelectMenuComponent | ButtonComponent] + | [ButtonComponent, ButtonComponent] + | [ButtonComponent, ButtonComponent, ButtonComponent] + | [ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent] + | [ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent]; } diff --git a/src/types/messages/components/button_data.ts b/src/types/messages/components/button_data.ts new file mode 100644 index 000000000..6080abfe8 --- /dev/null +++ b/src/types/messages/components/button_data.ts @@ -0,0 +1,6 @@ +export interface ButtonData { + /** with the value you defined for this component */ + customId: string; + /** The type of this component */ + componentType: 2; +} diff --git a/src/types/messages/components/message_component_types.ts b/src/types/messages/components/message_component_types.ts index 18e37875e..410653c0b 100644 --- a/src/types/messages/components/message_component_types.ts +++ b/src/types/messages/components/message_component_types.ts @@ -4,6 +4,8 @@ export enum DiscordMessageComponentTypes { ActionRow = 1, /** A button! */ Button, + /** A select menu. */ + SelectMenu, } export type MessageComponentTypes = DiscordMessageComponentTypes; diff --git a/src/types/messages/components/message_components.ts b/src/types/messages/components/message_components.ts index 045b0f75a..d0d9e5d8d 100644 --- a/src/types/messages/components/message_components.ts +++ b/src/types/messages/components/message_components.ts @@ -1,6 +1,7 @@ import { ActionRow } from "./action_row.ts"; import { ButtonComponent } from "./button_component.ts"; +import { SelectMenuComponent } from "./select_menu.ts"; -export type MessageComponent = ActionRow | ButtonComponent; +export type ActionRoleComponents = ButtonComponent | SelectMenuComponent; -export type MessageComponents = MessageComponent[]; +export type MessageComponents = ActionRow[]; diff --git a/src/types/messages/components/select_data.ts b/src/types/messages/components/select_data.ts new file mode 100644 index 000000000..241498695 --- /dev/null +++ b/src/types/messages/components/select_data.ts @@ -0,0 +1,8 @@ +export interface SelectMenuData { + /** The type of component */ + componentType: 3; + /** The custom id provided for this component. */ + customId: string; + /** The values chosen by the user. */ + values: string[]; +} diff --git a/src/types/messages/components/select_menu.ts b/src/types/messages/components/select_menu.ts new file mode 100644 index 000000000..6ddf342ef --- /dev/null +++ b/src/types/messages/components/select_menu.ts @@ -0,0 +1,14 @@ +import { SelectOption } from "./select_option.ts"; + +export interface SelectMenuComponent { + /** A custom identifier for this component. Maximum 100 characters. */ + customId: string; + /** A custom placeholder text if nothing is selected. Maximum 100 characters. */ + placeholder?: string; + /** The minimum number of items that must be selected. Default 1. Between 1-25. */ + minValues?: number; + /** The maximum number of items that can be selected. Default 1. Between 1-25. */ + maxValues?: number; + /** The choices! Maximum of 25 items. */ + options: SelectOption[]; +} \ No newline at end of file diff --git a/src/types/messages/components/select_option.ts b/src/types/messages/components/select_option.ts new file mode 100644 index 000000000..390226a31 --- /dev/null +++ b/src/types/messages/components/select_option.ts @@ -0,0 +1,21 @@ +export interface SelectOption { + /** The user-facing name of the option. Maximum 25 characters. */ + label: string; + /** The dev-defined value of the option. Maximum 100 characters. */ + value: string; + /** An additional description of the option. Maximum 50 characters. */ + description?: string; + /** The id, name, and animated properties of an emoji. */ + emoji?: + | string + | { + /** Emoji id */ + id?: string; + /** Emoji name */ + name?: string; + /** Whether this emoji is animated */ + animated?: boolean; + }; + /** Will render this option as already-selected by default. */ + default: boolean; +} diff --git a/src/types/messages/mod.ts b/src/types/messages/mod.ts index 3b700f956..2d8486264 100644 --- a/src/types/messages/mod.ts +++ b/src/types/messages/mod.ts @@ -6,6 +6,9 @@ export * from "./components/button_component.ts"; export * from "./components/button_styles.ts"; export * from "./components/message_component_types.ts"; export * from "./components/message_components.ts"; +export * from "./components/select_data.ts"; +export * from "./components/select_menu.ts"; +export * from "./components/select_option.ts"; export * from "./create_message.ts"; export * from "./edit_message.ts"; export * from "./get_messages.ts"; diff --git a/src/util/utils.ts b/src/util/utils.ts index 43d48466e..4e69fc8a5 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -1,6 +1,5 @@ import { encode } from "./deps.ts"; import { eventHandlers } from "../bot.ts"; -import { isActionRow } from "../helpers/type_guards/is_action_row.ts"; import { isButton } from "../helpers/type_guards/is_button.ts"; import { Errors } from "../types/discordeno/errors.ts"; import type { ApplicationCommandOption } from "../types/interactions/commands/application_command_option.ts"; @@ -215,43 +214,6 @@ export function validateComponents(components: MessageComponents) { let actionRowCounter = 0; for (const component of components) { - // 5 Link buttons can not have a customId - if (isButton(component)) { - if (component.type === ButtonStyles.Link && component.customId) { - throw new Error(Errors.LINK_BUTTON_CANNOT_HAVE_CUSTOM_ID); - } - // Other buttons must have a customId - if (!component.customId && component.type !== ButtonStyles.Link) { - throw new Error(Errors.BUTTON_REQUIRES_CUSTOM_ID); - } - - if (!validateLength(component.label, { max: 80 })) { - throw new Error(Errors.COMPONENT_LABEL_TOO_BIG); - } - - if (component.customId && !validateLength(component.customId, { max: 100 })) { - throw new Error(Errors.COMPONENT_CUSTOM_ID_TOO_BIG); - } - - if (typeof component.emoji === "string") { - // A snowflake id was provided - if (/^[0-9]+$/.test(component.emoji)) { - component.emoji = { - id: component.emoji, - }; - } else { - // A unicode emoji was provided - component.emoji = { - name: component.emoji, - }; - } - } - } - - if (!isActionRow(component)) { - continue; - } - actionRowCounter++; // Max of 5 ActionRows per message if (actionRowCounter > 5) throw new Error(Errors.TOO_MANY_ACTION_ROWS); @@ -260,5 +222,40 @@ export function validateComponents(components: MessageComponents) { if (component.components?.length > 5) { throw new Error(Errors.TOO_MANY_COMPONENTS); } + + for (const subcomponent of component.components) { + // 5 Link buttons can not have a customId + if (isButton(subcomponent)) { + if (subcomponent.type === ButtonStyles.Link && subcomponent.customId) { + throw new Error(Errors.LINK_BUTTON_CANNOT_HAVE_CUSTOM_ID); + } + // Other buttons must have a customId + if (!subcomponent.customId && subcomponent.type !== ButtonStyles.Link) { + throw new Error(Errors.BUTTON_REQUIRES_CUSTOM_ID); + } + + if (!validateLength(subcomponent.label, { max: 80 })) { + throw new Error(Errors.subcomponent_LABEL_TOO_BIG); + } + + if (subcomponent.customId && !validateLength(subcomponent.customId, { max: 100 })) { + throw new Error(Errors.subcomponent_CUSTOM_ID_TOO_BIG); + } + + if (typeof subcomponent.emoji === "string") { + // A snowflake id was provided + if (/^[0-9]+$/.test(subcomponent.emoji)) { + subcomponent.emoji = { + id: subcomponent.emoji, + }; + } else { + // A unicode emoji was provided + subcomponent.emoji = { + name: subcomponent.emoji, + }; + } + } + } + } } }