diff --git a/.changeset/chilly-owls-vanish.md b/.changeset/chilly-owls-vanish.md new file mode 100644 index 00000000..d3cb2a90 --- /dev/null +++ b/.changeset/chilly-owls-vanish.md @@ -0,0 +1,6 @@ +--- +"@buape/carbon": minor +--- + +feat: add MessagePayload for replies and message sending +This will allow you to pass just a string to reply with as the content, or the entire message payload. diff --git a/apps/rocko/src/commands/testing/button.ts b/apps/rocko/src/commands/testing/button.ts index 16b42c9e..3c2a7e61 100644 --- a/apps/rocko/src/commands/testing/button.ts +++ b/apps/rocko/src/commands/testing/button.ts @@ -29,7 +29,7 @@ class PingButton extends Button { style = ButtonStyle.Primary async run(interaction: ButtonInteraction) { - await interaction.reply({ content: "OMG YOU CLICKED THE BUTTON" }) + await interaction.reply("OMG YOU CLICKED THE BUTTON") } } diff --git a/packages/carbon/src/abstracts/BaseComponentInteraction.ts b/packages/carbon/src/abstracts/BaseComponentInteraction.ts index 1cb0c871..b2c115d6 100644 --- a/packages/carbon/src/abstracts/BaseComponentInteraction.ts +++ b/packages/carbon/src/abstracts/BaseComponentInteraction.ts @@ -6,13 +6,13 @@ import { Routes } from "discord-api-types/v10" import type { Client } from "../classes/Client.js" -import { splitCustomId } from "../utils.js" +import { serializePayload, splitCustomId } from "../utils.js" import { BaseInteraction, type InteractionDefaults, - type InteractionReplyData, type InteractionReplyOptions } from "./BaseInteraction.js" +import type { MessagePayload } from "../types.js" export class BaseComponentInteraction extends BaseInteraction { customId: string @@ -49,18 +49,17 @@ export class BaseComponentInteraction extends BaseInteraction = {} ) { + const serialized = serializePayload(data) await this.client.rest.post( Routes.interactionCallback(this.rawData.id, this.rawData.token), { body: { type: InteractionResponseType.UpdateMessage, data: { - ...data, - embeds: data.embeds?.map((embed) => embed.serialize()), - components: data.components?.map((row) => row.serialize()) + ...serialized } } as RESTPostAPIInteractionCallbackJSONBody, files: options.files diff --git a/packages/carbon/src/abstracts/BaseGuildChannel.ts b/packages/carbon/src/abstracts/BaseGuildChannel.ts index 262da766..74841b3a 100644 --- a/packages/carbon/src/abstracts/BaseGuildChannel.ts +++ b/packages/carbon/src/abstracts/BaseGuildChannel.ts @@ -1,6 +1,5 @@ import { type APIGuildChannel, - type APIMessage, type GuildChannelType, type RESTGetAPIGuildInvitesResult, type RESTPostAPIChannelInviteJSONBody, @@ -11,6 +10,8 @@ import { Guild } from "../structures/Guild.js" import type { GuildCategoryChannel } from "../structures/GuildCategoryChannel.js" import type { IfPartial } from "../utils.js" import { BaseChannel } from "./BaseChannel.js" +import type { MessagePayload } from "../types.js" +import { serializePayload } from "../utils.js" export abstract class BaseGuildChannel< Type extends GuildChannelType, @@ -132,9 +133,9 @@ export abstract class BaseGuildChannel< /** * Send a message to the channel */ - async send(message: APIMessage) { + async send(message: MessagePayload) { this.client.rest.post(Routes.channelMessages(this.id), { - body: { ...message } + body: serializePayload(message) }) } diff --git a/packages/carbon/src/abstracts/BaseInteraction.ts b/packages/carbon/src/abstracts/BaseInteraction.ts index 6963e5dc..3e2c6950 100644 --- a/packages/carbon/src/abstracts/BaseInteraction.ts +++ b/packages/carbon/src/abstracts/BaseInteraction.ts @@ -15,29 +15,12 @@ import { Guild, Message, type Modal, - type Row, User, channelFactory } from "../index.js" import { GuildMember } from "../structures/GuildMember.js" - -/** - * The data to reply to an interaction - */ -export type InteractionReplyData = { - /** - * The content of the message - */ - content?: string - /** - * The embeds of the message - */ - embeds?: Embed[] - /** - * The components to send in the message, listed in rows - */ - components?: Row[] -} +import { serializePayload } from "../utils.js" +import type { MessagePayload } from "../types.js" /** * Additional options for replying to an interaction @@ -148,10 +131,8 @@ export abstract class BaseInteraction extends Base { * If the interaction is deferred, this will edit the original response. * @param data The response data */ - async reply( - data: InteractionReplyData, - options: InteractionReplyOptions = {} - ) { + async reply(data: MessagePayload, options: InteractionReplyOptions = {}) { + const serialized = serializePayload(data) if (this._deferred) { await this.client.rest.patch( Routes.webhookMessage( @@ -161,10 +142,7 @@ export abstract class BaseInteraction extends Base { ), { body: { - ...data, - embeds: data.embeds?.map((embed) => embed.serialize()), - components: data.components?.map((row) => row.serialize()), - flags: options.ephemeral ? 64 : undefined + ...serialized } as RESTPatchAPIInteractionOriginalResponseJSONBody, files: options.files } @@ -176,10 +154,10 @@ export abstract class BaseInteraction extends Base { body: { type: InteractionResponseType.ChannelMessageWithSource, data: { - ...data, - embeds: data.embeds?.map((embed) => embed.serialize()), - components: data.components?.map((row) => row.serialize()), - flags: options.ephemeral ? 64 : undefined + ...serializePayload(data), + flags: options.ephemeral + ? 64 | ("flags" in serialized ? (serialized.flags ?? 0) : 0) + : undefined } } as RESTPostAPIInteractionCallbackJSONBody, files: options.files @@ -228,18 +206,16 @@ export abstract class BaseInteraction extends Base { /** * Send a followup message to the interaction */ - async followUp( - reply: InteractionReplyData, - options: InteractionReplyOptions = {} - ) { + async followUp(reply: MessagePayload, options: InteractionReplyOptions = {}) { + const serialized = serializePayload(reply) await this.client.rest.post( Routes.webhook(this.client.options.clientId, this.rawData.token), { body: { - ...reply, - embeds: reply.embeds?.map((embed) => embed.serialize()), - components: reply.components?.map((row) => row.serialize()), - flags: options.ephemeral ? 64 : undefined + ...serialized, + flags: options.ephemeral + ? 64 | ("flags" in serialized ? (serialized.flags ?? 0) : 0) + : undefined } as RESTPostAPIInteractionFollowupJSONBody, files: options.files } diff --git a/packages/carbon/src/abstracts/GuildThreadOnlyChannel.ts b/packages/carbon/src/abstracts/GuildThreadOnlyChannel.ts index b51a02bd..25e23ae4 100644 --- a/packages/carbon/src/abstracts/GuildThreadOnlyChannel.ts +++ b/packages/carbon/src/abstracts/GuildThreadOnlyChannel.ts @@ -1,7 +1,6 @@ import type { APIGuildForumDefaultReactionEmoji, APIGuildForumTag, - APIMessage, APIThreadOnlyChannel, ChannelType, SortOrderType, @@ -10,6 +9,7 @@ import type { import { GuildThreadChannel } from "../structures/GuildThreadChannel.js" import type { IfPartial } from "../utils.js" import { BaseGuildChannel } from "./BaseGuildChannel.js" +import type { MessagePayload } from "../types.js" export abstract class GuildThreadOnlyChannel< Type extends ChannelType.GuildForum | ChannelType.GuildMedia, @@ -83,7 +83,7 @@ export abstract class GuildThreadOnlyChannel< * @remarks * This is an alias for {@link GuildThreadChannel.send} that will fetch the channel, but if you already have the channel, you can use {@link GuildThreadChannel.send} instead. */ - async sendToPost(message: APIMessage, postId: string): Promise { + async sendToPost(message: MessagePayload, postId: string): Promise { const channel = new GuildThreadChannel( this.client, postId diff --git a/packages/carbon/src/structures/DmChannel.ts b/packages/carbon/src/structures/DmChannel.ts index 520f0166..0359a4d6 100644 --- a/packages/carbon/src/structures/DmChannel.ts +++ b/packages/carbon/src/structures/DmChannel.ts @@ -1,11 +1,11 @@ import { type APIDMChannel, - type APIMessage, type ChannelType, Routes } from "discord-api-types/v10" import { BaseChannel } from "../abstracts/BaseChannel.js" -import type { IfPartial } from "../utils.js" +import { serializePayload, type IfPartial } from "../utils.js" +import type { MessagePayload } from "../types.js" /** * Represents a DM between two users. @@ -27,9 +27,9 @@ export class DmChannel extends BaseChannel< /** * Send a message to the channel */ - async send(message: APIMessage) { + async send(message: MessagePayload) { this.client.rest.post(Routes.channelMessages(this.id), { - body: { ...message } + body: serializePayload(message) }) } } diff --git a/packages/carbon/src/structures/User.ts b/packages/carbon/src/structures/User.ts index bf4df38c..2566d32a 100644 --- a/packages/carbon/src/structures/User.ts +++ b/packages/carbon/src/structures/User.ts @@ -2,7 +2,6 @@ import { type APIDMChannel, type APIMessage, type APIUser, - type RESTPostAPIChannelMessageJSONBody, Routes, type UserFlags } from "discord-api-types/v10" @@ -10,6 +9,8 @@ import { Base } from "../abstracts/Base.js" import type { Client } from "../classes/Client.js" import type { IfPartial } from "../utils.js" import { Message } from "./Message.js" +import { serializePayload } from "../utils.js" +import type { MessagePayload } from "../types.js" export class User extends Base { constructor( @@ -172,14 +173,12 @@ export class User extends Base { /** * Send a message to this user. */ - async send(data: RESTPostAPIChannelMessageJSONBody) { + async send(data: MessagePayload) { const dmChannel = await this.createDm(this.id) const message = (await this.client.rest.post( Routes.channelMessages(dmChannel.id), { - body: { - ...data - } + body: serializePayload(data) } )) as APIMessage return new Message(this.client, message) diff --git a/packages/carbon/src/types.ts b/packages/carbon/src/types.ts new file mode 100644 index 00000000..34bdde0f --- /dev/null +++ b/packages/carbon/src/types.ts @@ -0,0 +1,39 @@ +import type { Embed } from "./classes/Embed.js" +import type { Row } from "./classes/Row.js" + +/** + * The data that is sent to Discord when sending a message. + * If you pass just a string, it will be treated as the content of the message. + */ +export type MessagePayload = + | { + /** + * The content of the message + */ + content?: string + /** + * The embeds of the message + */ + embeds?: Embed[] + /** + * The components to send in the message, listed in rows + */ + components?: Row[] + /** + * The settings for which mentions are allowed in the message + */ + allowedMentions?: { + parse?: ["roles", "users", "everyone"] + roles?: string[] + users?: string[] + } + /** + * The flags for the message + */ + flags?: number + /** + * Whether the message should be TTS + */ + tts?: boolean + } + | string diff --git a/packages/carbon/src/utils.ts b/packages/carbon/src/utils.ts index 2c29aa07..620d0d98 100644 --- a/packages/carbon/src/utils.ts +++ b/packages/carbon/src/utils.ts @@ -1,4 +1,5 @@ export type IfPartial = T extends true ? V : U +import type { MessagePayload } from "./types.js" export const splitCustomId = ( customId: string @@ -80,3 +81,13 @@ export function concatUint8Arrays( merged.set(arr2, arr1.length) return merged } + +export const serializePayload = (payload: MessagePayload) => { + return typeof payload === "string" + ? { content: payload } + : { + ...payload, + embeds: payload.embeds?.map((embed) => embed.serialize()), + components: payload.components?.map((row) => row.serialize()) + } +}