diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index f523f59b4..784c741b6 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -70,6 +70,7 @@ exports.WebSocketManager = require('./client/websocket/WebSocketManager'); exports.WebSocketShard = require('./client/websocket/WebSocketShard'); // Structures +exports.ActionRow = require('./structures/ActionRow'); exports.Activity = require('./structures/Presence').Activity; exports.AnonymousGuild = require('./structures/AnonymousGuild'); exports.Application = require('./structures/interfaces/Application'); @@ -80,6 +81,7 @@ exports.BaseGuild = require('./structures/BaseGuild'); exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji'); exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel'); exports.BaseGuildVoiceChannel = require('./structures/BaseGuildVoiceChannel'); +exports.ButtonComponent = require('./structures/ButtonComponent'); exports.ButtonInteraction = require('./structures/ButtonInteraction'); exports.CategoryChannel = require('./structures/CategoryChannel'); exports.Channel = require('./structures/Channel').Channel; @@ -92,7 +94,7 @@ exports.Collector = require('./structures/interfaces/Collector'); exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver'); exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction'); exports.DMChannel = require('./structures/DMChannel'); -exports.Embed = require('@discordjs/builders').Embed; +exports.Embed = require('./structures/Embed'); exports.UnsafeEmbed = require('@discordjs/builders').UnsafeEmbed; exports.Emoji = require('./structures/Emoji').Emoji; exports.Guild = require('./structures/Guild').Guild; @@ -131,6 +133,7 @@ exports.ReactionCollector = require('./structures/ReactionCollector'); exports.ReactionEmoji = require('./structures/ReactionEmoji'); exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets; exports.Role = require('./structures/Role').Role; +exports.SelectMenuComponent = require('./structures/SelectMenuComponent'); exports.SelectMenuInteraction = require('./structures/SelectMenuInteraction'); exports.StageChannel = require('./structures/StageChannel'); exports.StageInstance = require('./structures/StageInstance').StageInstance; @@ -190,10 +193,7 @@ exports.StickerType = require('discord-api-types/v9').StickerType; exports.StickerFormatType = require('discord-api-types/v9').StickerFormatType; exports.UserFlags = require('discord-api-types/v9').UserFlags; exports.WebhookType = require('discord-api-types/v9').WebhookType; -exports.ActionRow = require('@discordjs/builders').ActionRow; -exports.ButtonComponent = require('@discordjs/builders').ButtonComponent; exports.UnsafeButtonComponent = require('@discordjs/builders').UnsafeButtonComponent; -exports.SelectMenuComponent = require('@discordjs/builders').SelectMenuComponent; exports.UnsafeSelectMenuComponent = require('@discordjs/builders').UnsafeSelectMenuComponent; exports.SelectMenuOption = require('@discordjs/builders').SelectMenuOption; exports.UnsafeSelectMenuOption = require('@discordjs/builders').UnsafeSelectMenuOption; diff --git a/packages/discord.js/src/structures/ActionRow.js b/packages/discord.js/src/structures/ActionRow.js new file mode 100644 index 000000000..2f050f770 --- /dev/null +++ b/packages/discord.js/src/structures/ActionRow.js @@ -0,0 +1,14 @@ +'use strict'; + +const { ActionRow: BuildersActionRow } = require('@discordjs/builders'); +const Components = require('../util/Components'); + +class ActionRow extends BuildersActionRow { + constructor(data) { + // TODO: Simplify when getters PR is merged. + const initData = Components.transformJSON(data); + super({ ...initData, components: initData.components ?? [] }); + } +} + +module.exports = ActionRow; diff --git a/packages/discord.js/src/structures/ButtonComponent.js b/packages/discord.js/src/structures/ButtonComponent.js new file mode 100644 index 000000000..e25705399 --- /dev/null +++ b/packages/discord.js/src/structures/ButtonComponent.js @@ -0,0 +1,12 @@ +'use strict'; + +const { ButtonComponent: BuildersButtonComponent } = require('@discordjs/builders'); +const Components = require('../util/Components'); + +class ButtonComponent extends BuildersButtonComponent { + constructor(data) { + super(Components.transformJSON(data)); + } +} + +module.exports = ButtonComponent; diff --git a/packages/discord.js/src/structures/Embed.js b/packages/discord.js/src/structures/Embed.js new file mode 100644 index 000000000..e95ebf595 --- /dev/null +++ b/packages/discord.js/src/structures/Embed.js @@ -0,0 +1,12 @@ +'use strict'; + +const { Embed: BuildersEmbed } = require('@discordjs/builders'); +const Embeds = require('../util/Embeds'); + +class Embed extends BuildersEmbed { + constructor(data) { + super({ ...Embeds.transformJSON(data) }); + } +} + +module.exports = Embed; diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 219b5a6ee..72fa372b3 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -1,10 +1,12 @@ 'use strict'; const { Buffer } = require('node:buffer'); -const { createComponent, Embed } = require('@discordjs/builders'); +const { Embed, isJSONEncodable } = require('@discordjs/builders'); const { MessageFlags } = require('discord-api-types/v9'); const { RangeError } = require('../errors'); +const Components = require('../util/Components'); const DataResolver = require('../util/DataResolver'); +const Embeds = require('../util/Embeds'); const MessageFlagsBitField = require('../util/MessageFlagsBitField'); const Util = require('../util/Util'); @@ -131,7 +133,9 @@ class MessagePayload { } } - const components = this.options.components?.map(c => createComponent(c).toJSON()); + const components = this.options.components?.map(c => + isJSONEncodable(c) ? c.toJSON() : Components.transformJSON(c), + ); let username; let avatarURL; @@ -190,7 +194,9 @@ class MessagePayload { content, tts, nonce, - embeds: this.options.embeds?.map(embed => (embed instanceof Embed ? embed : new Embed(embed)).toJSON()), + embeds: this.options.embeds?.map(embed => + embed instanceof Embed ? embed.toJSON() : Embeds.transformJSON(embed), + ), components, username, avatar_url: avatarURL, diff --git a/packages/discord.js/src/structures/SelectMenuComponent.js b/packages/discord.js/src/structures/SelectMenuComponent.js new file mode 100644 index 000000000..dc7dec046 --- /dev/null +++ b/packages/discord.js/src/structures/SelectMenuComponent.js @@ -0,0 +1,12 @@ +'use strict'; + +const { SelectMenuComponent: BuildersSelectMenuComponent } = require('@discordjs/builders'); +const Components = require('../util/Components'); + +class SelectMenuComponent extends BuildersSelectMenuComponent { + constructor(data) { + super(Components.transformJSON(data)); + } +} + +module.exports = SelectMenuComponent; diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js new file mode 100644 index 000000000..52330d409 --- /dev/null +++ b/packages/discord.js/src/util/Components.js @@ -0,0 +1,70 @@ +'use strict'; + +/** + * @typedef {Object} BaseComponentData + * @property {ComponentType} type + */ + +/** + * @typedef {BaseComponentData} ActionRowData + * @property {ComponentData[]} components + */ + +/** + * @typedef {BaseComponentData} ButtonComponentData + * @property {ButtonStyle} style + * @property {?boolean} disabled + * @property {string} label + * @property {?APIComponentEmoji} emoji + * @property {?string} customId + * @property {?string} url + */ + +/** + * @typedef {object} SelectMenuComponentOptionData + * @property {string} label + * @property {string} value + * @property {?string} description + * @property {?APIComponentEmoji} emoji + * @property {?boolean} default + */ + +/** + * @typedef {BaseComponentData} SelectMenuComponentData + * @property {string} customId + * @property {?boolean} disabled + * @property {?number} maxValues + * @property {?number} minValues + * @property {?SelectMenuComponentOptionData[]} options + * @property {?string} placeholder + */ + +/** + * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData} ComponentData + */ + +class Components extends null { + /** + * Transforms json data into api-compatible json data. + * @param {ComponentData|APIMessageComponent} data The data to transform. + * @returns {APIMessageComponentData} + */ + static transformJSON(data) { + return { + type: data?.type, + custom_id: data?.customId ?? data?.custom_id, + disabled: data?.disabled, + style: data?.style, + label: data?.label, + emoji: data?.emoji, + url: data?.url, + options: data?.options, + placeholder: data?.placeholder, + min_values: data?.minValues ?? data?.min_values, + max_values: data?.maxValues ?? data?.max_values, + components: data?.components?.map(c => Components.transformJSON(c)), + }; + } +} + +module.exports = Components; diff --git a/packages/discord.js/src/util/Embeds.js b/packages/discord.js/src/util/Embeds.js new file mode 100644 index 000000000..b0cf3a3cd --- /dev/null +++ b/packages/discord.js/src/util/Embeds.js @@ -0,0 +1,81 @@ +'use strict'; + +/** + * @typedef {Object} EmbedData + * @property {?string} title + * @property {?EmbedType} type + * @property {?string} description + * @property {?string} url + * @property {?string} timestamp + * @property {?number} color + * @property {?EmbedFooterData} footer + * @property {?EmbedImageData} image + * @property {?EmbedImageData} thumbnail + * @property {?EmbedProviderData} provider + * @property {?EmbedAuthorData} author + * @property {?EmbedFieldData[]} fields + */ + +/** + * @typedef {Object} EmbedFooterData + * @property {string} text + * @property {?string} iconURL + */ + +/** + * @typedef {Object} EmbedImageData + * @property {?string} url + */ + +/** + * @typedef {Object} EmbedProviderData + * @property {?string} name + * @property {?string} url + */ + +/** + * @typedef {Object} EmbedAuthorData + * @property {string} name + * @property {?string} url + * @property {?string} iconURL + */ + +/** + * @typedef {Object} EmbedFieldData + * @property {string} name + * @property {string} value + * @property {?boolean} inline + */ + +class Embeds extends null { + /** + * Transforms json data into api-compatible json data. + * @param {EmbedData|APIEmbed} data The data to transform. + * @returns {APIEmbed} + */ + static transformJSON(data) { + return { + title: data?.title, + type: data?.type, + description: data?.description, + url: data?.url, + timestamp: data?.timestamp, + color: data?.color, + footer: { + test: data?.footer?.text, + icon_url: data?.footer?.iconURL ?? data?.footer?.icon_url, + }, + image: data?.image, + thumbnail: data?.thumbnail, + provider: data?.provider, + author: { + name: data?.author?.name, + text: data?.author?.text, + icon_url: data?.author?.iconURL ?? data?.author?.icon_url, + }, + fields: data?.fields, + }; + } +} + +module.exports = Embeds; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index c90d27429..764ed7a74 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -1,13 +1,13 @@ import { - ActionRow, + ActionRow as BuilderActionRow, ActionRowComponent, blockQuote, bold, - ButtonComponent, + ButtonComponent as BuilderButtonComponent, channelMention, codeBlock, Component, - Embed, + Embed as BuildersEmbed, formatEmoji, hideLinkEmbed, hyperlink, @@ -16,7 +16,7 @@ import { memberNicknameMention, quote, roleMention, - SelectMenuComponent, + SelectMenuComponent as BuilderSelectMenuComponent, spoiler, strikethrough, time, @@ -90,6 +90,8 @@ import { GuildSystemChannelFlags, GatewayIntentBits, ActivityFlags, + APIMessageComponentEmoji, + EmbedType, } from 'discord-api-types/v9'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -188,6 +190,20 @@ export class Activity { export type ActivityFlagsString = keyof typeof ActivityFlags; +export interface BaseComponentData { + type?: ComponentType; +} + +export type ActionRowComponentData = ButtonComponentData | SelectMenuComponentData; + +export interface ActionRowData extends BaseComponentData { + components: ActionRowComponentData[]; +} + +export class ActionRow extends BuilderActionRow { + constructor(data?: ActionRowData | APIActionRowComponent); +} + export class ActivityFlagsBitField extends BitField { public static Flags: typeof ActivityFlags; public static resolve(bit?: BitFieldResolvable): number; @@ -451,6 +467,42 @@ export class ButtonInteraction extends Mes public inRawGuild(): this is ButtonInteraction<'raw'>; } +export class ButtonComponent extends BuilderButtonComponent { + public constructor(data?: ButtonComponentData | APIButtonComponent); +} + +export class SelectMenuComponent extends BuilderSelectMenuComponent { + public constructor(data?: SelectMenuComponentData | APISelectMenuComponent); +} + +export interface EmbedData { + title?: string; + type?: EmbedType; + description?: string; + url?: string; + timestamp?: string; + color?: number; + footer?: EmbedFooterData; + image?: EmbedImageData; + thumbnail?: EmbedImageData; + provider?: EmbedProviderData; + author?: EmbedAuthorData; + fields?: EmbedFieldData[]; +} + +export interface EmbedImageData { + url?: string; +} + +export interface EmbedProviderData { + name?: string; + url?: string; +} + +export class Embed extends BuildersEmbed { + public constructor(data?: EmbedData | APIEmbed); +} + export interface MappedChannelCategoryTypes { [ChannelType.GuildNews]: NewsChannel; [ChannelType.GuildVoice]: VoiceChannel; @@ -2354,6 +2406,18 @@ export class Formatters extends null { public static userMention: typeof userMention; } +export type ComponentData = ActionRowComponentData | ButtonComponentData | SelectMenuComponentData; + +export class Components extends null { + private constructor(); + public static transformJSON(data: ComponentData | APIMessageComponent): APIMessageComponent; +} + +export class Embeds extends null { + private constructor(); + public static transformJSON(data: EmbedData | APIEmbed): APIEmbed; +} + export class VoiceChannel extends BaseGuildVoiceChannel { public readonly speakable: boolean; public type: ChannelType.GuildVoice; @@ -3354,10 +3418,6 @@ export interface ThreadMemberFetchOptions extends BaseFetchOptions { member?: UserResolvable; } -export interface BaseMessageComponentOptions { - type?: ComponentType; -} - export type BitFieldResolvable = | RecursiveReadonlyArray>> | T @@ -4469,37 +4529,33 @@ export interface MakeErrorOptions { export type MemberMention = UserMention | `<@!${Snowflake}>`; export type ActionRowComponentOptions = - | (Required & MessageButtonOptions) - | (Required & MessageSelectMenuOptions); + | (Required & ButtonComponentData) + | (Required & SelectMenuComponentData); export type MessageActionRowComponentResolvable = ActionRowComponent | ActionRowComponentOptions; -export interface ActionRowOptions extends BaseMessageComponentOptions { - components: ActionRowComponent[]; -} - export interface MessageActivity { partyId: string; type: number; } -export interface BaseButtonOptions extends BaseMessageComponentOptions { +export interface BaseButtonComponentData extends BaseComponentData { disabled?: boolean; - emoji?: EmojiIdentifierResolvable; + emoji?: APIMessageComponentEmoji; label?: string; } -export interface LinkButtonOptions extends BaseButtonOptions { - style: 'Link' | ButtonStyle.Link; +export interface LinkButtonComponentData extends BaseButtonComponentData { + style: ButtonStyle.Link; url: string; } -export interface InteractionButtonOptions extends BaseButtonOptions { +export interface InteractionButtonComponentData extends BaseButtonComponentData { style: Exclude; customId: string; } -export type MessageButtonOptions = InteractionButtonOptions | LinkButtonOptions; +export type ButtonComponentData = InteractionButtonComponentData | LinkButtonComponentData; export interface MessageCollectorOptions extends CollectorOptions<[Message]> { max?: number; @@ -4518,12 +4574,6 @@ export type MessageChannelComponentCollectorOptions; -export type MessageComponentOptions = - | BaseMessageComponentOptions - | ActionRowOptions - | MessageButtonOptions - | MessageSelectMenuOptions; - export interface MessageEditOptions { attachments?: MessageAttachment[]; content?: string | null; @@ -4531,7 +4581,7 @@ export interface MessageEditOptions { files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; - components?: (ActionRow | (Required & ActionRowOptions))[]; + components?: (ActionRow | (Required & ActionRowData))[]; } export interface MessageEvent { @@ -4568,7 +4618,7 @@ export interface MessageOptions { nonce?: string | number; content?: string | null; embeds?: (Embed | APIEmbed)[]; - components?: (ActionRow | (Required & ActionRowOptions))[]; + components?: (ActionRow | (Required & ActionRowData))[]; allowedMentions?: MessageMentionOptions; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; reply?: ReplyOptions; @@ -4593,12 +4643,12 @@ export interface MessageReference { export type MessageResolvable = Message | Snowflake; -export interface MessageSelectMenuOptions extends BaseMessageComponentOptions { +export interface SelectMenuComponentData extends BaseComponentData { customId?: string; disabled?: boolean; maxValues?: number; minValues?: number; - options?: MessageSelectOptionData[]; + options?: SelectMenuComponentOptionData[]; placeholder?: string; } @@ -4610,10 +4660,10 @@ export interface MessageSelectOption { value: string; } -export interface MessageSelectOptionData { +export interface SelectMenuComponentOptionData { default?: boolean; description?: string; - emoji?: EmojiIdentifierResolvable; + emoji?: APIMessageComponentEmoji; label: string; value: string; } @@ -5116,15 +5166,11 @@ export { WebhookType, } from 'discord-api-types/v9'; export { - ActionRow, - ButtonComponent, UnsafeButtonComponent, - SelectMenuComponent, UnsafeSelectMenuComponent, SelectMenuOption, UnsafeSelectMenuOption, ActionRowComponent, - Embed, UnsafeEmbed, } from '@discordjs/builders'; export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest'; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 7acb15089..814adac9d 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -16,6 +16,7 @@ import { InteractionType, GatewayIntentBits, PermissionFlagsBits, + ButtonStyle, } from 'discord-api-types/v9'; import { AuditLogEvent } from 'discord-api-types/v9'; import { @@ -1322,3 +1323,19 @@ expectType(GuildTextBasedChannel); + +const button = new ButtonComponent({ + label: 'test', + style: ButtonStyle.Primary, + customId: 'test', +}); + +const selectMenu = new SelectMenuComponent({ + maxValues: 10, + minValues: 2, + customId: 'test', +}); + +new ActionRow({ + components: [selectMenu, button], +});