diff --git a/.changeset/five-wolves-destroy.md b/.changeset/five-wolves-destroy.md new file mode 100644 index 0000000..4490d47 --- /dev/null +++ b/.changeset/five-wolves-destroy.md @@ -0,0 +1,5 @@ +--- +"reacord": minor +--- + +breaking: more descriptive component event types diff --git a/.changeset/many-pets-melt.md b/.changeset/many-pets-melt.md new file mode 100644 index 0000000..b8a8763 --- /dev/null +++ b/.changeset/many-pets-melt.md @@ -0,0 +1,33 @@ +--- +"reacord": minor +--- + +add new descriptive adapter methods + +The reacord instance names have been updated, and the old names are now deprecated. + +- `send` -> `createChannelMessage` +- `reply` -> `createInteractionReply` + +These new methods also accept discord JS options. Usage example: + +```ts +// can accept either a channel object or a channel ID +reacord.createChannelMessage(channel) +reacord.createChannelMessage(channel, { + tts: true, +}) +reacord.createChannelMessage(channel, { + reply: { + messageReference: "123456789012345678", + failIfNotExists: false, + }, +}) + +reacord.createInteractionReply(interaction) +reacord.createInteractionReply(interaction, { + ephemeral: true, +}) +``` + +These new methods replace the old ones, which are now deprecated. diff --git a/package.json b/package.json index a644767..f1b06fc 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,9 @@ "scripts": { "lint": "run-s --continue-on-error lint:*", "lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives", - "lint:prettier": "prettier . --write --cache --list-different", + "lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different", "lint:types": "tsc -b & pnpm -r --parallel run typecheck", "astro-sync": "pnpm --filter website exec astro sync", - "format": "run-s --continue-on-error format:*", - "format:eslint": "eslint . --report-unused-disable-directives --fix", - "format:prettier": "prettier --cache --write . \"**/*.astro\"", "test": "vitest", "build": "pnpm -r run build", "build:website": "pnpm --filter website... run build", diff --git a/packages/reacord/library/core/component-event.ts b/packages/reacord/library/core/component-event.ts index 62c7d6f..f85fd31 100644 --- a/packages/reacord/library/core/component-event.ts +++ b/packages/reacord/library/core/component-event.ts @@ -9,41 +9,52 @@ export interface ComponentEvent { * * @see https://discord.com/developers/docs/resources/channel#message-object */ - message: MessageInfo + message: ComponentEventMessage /** * The channel that this event occurred in. * * @see https://discord.com/developers/docs/resources/channel#channel-object */ - channel: ChannelInfo + channel: ComponentEventChannel /** * The user that triggered this event. * * @see https://discord.com/developers/docs/resources/user#user-object */ - user: UserInfo + user: ComponentEventUser /** * The guild that this event occurred in. * * @see https://discord.com/developers/docs/resources/guild#guild-object */ - guild?: GuildInfo + guild?: ComponentEventGuild /** Create a new reply to this event. */ - reply(content?: ReactNode): ReacordInstance + reply( + content?: ReactNode, + options?: ComponentEventReplyOptions, + ): ReacordInstance /** * Create an ephemeral reply to this event, shown only to the user who * triggered it. + * + * @deprecated Use event.reply(content, { ephemeral: true }) */ ephemeralReply(content?: ReactNode): ReacordInstance } /** @category Component Event */ -export interface ChannelInfo { +export interface ComponentEventReplyOptions { + ephemeral?: boolean + tts?: boolean +} + +/** @category Component Event */ +export interface ComponentEventChannel { id: string name?: string topic?: string @@ -55,11 +66,11 @@ export interface ChannelInfo { } /** @category Component Event */ -export interface MessageInfo { +export interface ComponentEventMessage { id: string channelId: string authorId: string - member?: GuildMemberInfo + member?: ComponentEventGuildMember content: string timestamp: string editedTimestamp?: string @@ -70,14 +81,14 @@ export interface MessageInfo { } /** @category Component Event */ -export interface GuildInfo { +export interface ComponentEventGuild { id: string name: string - member: GuildMemberInfo + member: ComponentEventGuildMember } /** @category Component Event */ -export interface GuildMemberInfo { +export interface ComponentEventGuildMember { id: string nick?: string displayName: string @@ -92,7 +103,7 @@ export interface GuildMemberInfo { } /** @category Component Event */ -export interface UserInfo { +export interface ComponentEventUser { id: string username: string discriminator: string diff --git a/packages/reacord/library/core/instance.ts b/packages/reacord/library/core/instance.ts index 4c6eca3..bea8ba7 100644 --- a/packages/reacord/library/core/instance.ts +++ b/packages/reacord/library/core/instance.ts @@ -7,7 +7,7 @@ import type { ReactNode } from "react" */ export interface ReacordInstance { /** Render some JSX to this instance (edits the message) */ - render: (content: ReactNode) => void + render: (content: ReactNode) => ReacordInstance /** Remove this message */ destroy: () => void diff --git a/packages/reacord/library/core/reacord-discord-js.ts b/packages/reacord/library/core/reacord-discord-js.ts index 6e48763..8a8cdff 100644 --- a/packages/reacord/library/core/reacord-discord-js.ts +++ b/packages/reacord/library/core/reacord-discord-js.ts @@ -14,24 +14,17 @@ import type { import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer" import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer" import type { - ChannelInfo, - GuildInfo, - GuildMemberInfo, - MessageInfo, - UserInfo, + ComponentEventChannel, + ComponentEventGuild, + ComponentEventGuildMember, + ComponentEventMessage, + ComponentEventReplyOptions, + ComponentEventUser, } from "./component-event" import type { ReacordInstance } from "./instance" import type { ReacordConfig } from "./reacord" import { Reacord } from "./reacord" -interface SendOptions { - reply?: boolean -} - -interface ReplyOptions { - ephemeral?: boolean -} - /** * The Reacord adapter for Discord.js. * @@ -54,17 +47,51 @@ export class ReacordDiscordJs extends Reacord { } /** - * Sends a message to a channel. Alternatively replies to message event. + * Sends a message to a channel. * + * @param target Discord channel object. + * @param [options] Options for the channel message * @see https://reacord.mapleleaf.dev/guides/sending-messages + * @see {@link Discord.MessageCreateOptions} */ - override send( - channelId: string, + public createChannelMessage( + target: Discord.ChannelResolvable, + options: Discord.MessageCreateOptions = {}, + ): ReacordInstance { + return this.createInstance( + this.createChannelMessageRenderer(target, options), + ) + } + + /** + * Replies to a command interaction by sending a message. + * + * @param interaction Discord command interaction object. + * @param [options] Custom options for the interaction reply method. + * @see https://reacord.mapleleaf.dev/guides/sending-messages + * @see {@link Discord.InteractionReplyOptions} + */ + public createInteractionReply( + interaction: Discord.CommandInteraction, + options: Discord.InteractionReplyOptions = {}, + ): ReacordInstance { + return this.createInstance( + this.createInteractionReplyRenderer(interaction, options), + ) + } + + /** + * Sends a message to a channel. + * + * @deprecated Use reacord.createChannelMessage() instead. + * @see https://reacord.mapleleaf.dev/guides/sending-messages + */ + public send( + channel: Discord.ChannelResolvable, initialContent?: React.ReactNode, - options?: SendOptions, ): ReacordInstance { return this.createInstance( - this.createChannelRenderer(channelId, options), + this.createChannelMessageRenderer(channel, {}), initialContent, ) } @@ -72,15 +99,15 @@ export class ReacordDiscordJs extends Reacord { /** * Sends a message as a reply to a command interaction. * + * @deprecated Use reacord.createInteractionReply() instead. * @see https://reacord.mapleleaf.dev/guides/sending-messages */ - override reply( + public reply( interaction: Discord.CommandInteraction, initialContent?: React.ReactNode, - options?: ReplyOptions, ): ReacordInstance { return this.createInstance( - this.createInteractionReplyRenderer(interaction, options), + this.createInteractionReplyRenderer(interaction, {}), initialContent, ) } @@ -88,51 +115,49 @@ export class ReacordDiscordJs extends Reacord { /** * Sends an ephemeral message as a reply to a command interaction. * - * @deprecated Use reacord.reply(interaction, content, { ephemeral: true }) + * @deprecated Use reacord.createInteractionReply(interaction, { ephemeral: + * true }) * @see https://reacord.mapleleaf.dev/guides/sending-messages */ - override ephemeralReply( + public ephemeralReply( interaction: Discord.CommandInteraction, initialContent?: React.ReactNode, - options?: Omit, ): ReacordInstance { return this.createInstance( this.createInteractionReplyRenderer(interaction, { - ...options, ephemeral: true, }), initialContent, ) } - private createChannelRenderer( - event: string | Discord.Message, - opts?: SendOptions, + private createChannelMessageRenderer( + channelResolvable: Discord.ChannelResolvable, + messageCreateOptions?: Discord.MessageCreateOptions, ) { return new ChannelMessageRenderer({ - send: async (options) => { - // Backwards compatible channelId api - // `event` is treated as MessageEvent depending on its type - const channel = - typeof event === "string" - ? this.client.channels.cache.get(event) ?? - (await this.client.channels.fetch(event)) ?? - raise(`Channel ${event} not found`) - : event.channel - - if (!channel.isTextBased()) { - raise(`Channel ${channel.id} is not a text channel`) + send: async (messageOptions) => { + let channel = this.client.channels.resolve(channelResolvable) + if (!channel && typeof channelResolvable === "string") { + channel = await this.client.channels.fetch(channelResolvable) } - if (opts?.reply) { - if (typeof event === "string") { - raise("Cannot send reply with channel ID provided") - } + if (!channel) { + const id = + typeof channelResolvable === "string" + ? channelResolvable + : channelResolvable.id + raise(`Channel ${id} not found`) + } - const message = await event.reply(getDiscordMessageOptions(options)) - return createReacordMessage(message) + if (!channel.isTextBased()) { + raise(`Channel ${channel.id} must be a text channel`) } - const message = await channel.send(getDiscordMessageOptions(options)) + + const message = await channel.send({ + ...getDiscordMessageOptions(messageOptions), + ...messageCreateOptions, + }) return createReacordMessage(message) }, }) @@ -142,24 +167,23 @@ export class ReacordDiscordJs extends Reacord { interaction: | Discord.CommandInteraction | Discord.MessageComponentInteraction, - opts?: ReplyOptions, + interactionReplyOptions: Discord.InteractionReplyOptions, ) { return new InteractionReplyRenderer({ - type: "command", - id: interaction.id, - reply: async (options) => { + interactionId: interaction.id, + reply: async (messageOptions) => { const message = await interaction.reply({ - ...getDiscordMessageOptions(options), + ...getDiscordMessageOptions(messageOptions), + ...interactionReplyOptions, fetchReply: true, - ephemeral: opts?.ephemeral, }) return createReacordMessage(message) }, - followUp: async (options) => { + followUp: async (messageOptions) => { const message = await interaction.followUp({ - ...getDiscordMessageOptions(options), + ...getDiscordMessageOptions(messageOptions), + ...interactionReplyOptions, fetchReply: true, - ephemeral: opts?.ephemeral, }) return createReacordMessage(message) }, @@ -170,7 +194,7 @@ export class ReacordDiscordJs extends Reacord { interaction: Discord.MessageComponentInteraction, ): ComponentInteraction { // todo please dear god clean this up - const channel: ChannelInfo = interaction.channel + const channel: ComponentEventChannel = interaction.channel ? { ...pruneNullishValues( pick(interaction.channel, [ @@ -186,7 +210,7 @@ export class ReacordDiscordJs extends Reacord { } : raise("Non-channel interactions are not supported") - const message: MessageInfo = + const message: ComponentEventMessage = interaction.message instanceof Discord.Message ? { ...pick(interaction.message, [ @@ -209,7 +233,7 @@ export class ReacordDiscordJs extends Reacord { } : raise("Message not found") - const member: GuildMemberInfo | undefined = + const member: ComponentEventGuildMember | undefined = interaction.member instanceof Discord.GuildMember ? { ...pruneNullishValues( @@ -234,14 +258,14 @@ export class ReacordDiscordJs extends Reacord { } : undefined - const guild: GuildInfo | undefined = interaction.guild + const guild: ComponentEventGuild | undefined = interaction.guild ? { ...pruneNullishValues(pick(interaction.guild, ["id", "name"])), member: member ?? raise("unexpected: member is undefined"), } : undefined - const user: UserInfo = { + const user: ComponentEventUser = { ...pruneNullishValues( pick(interaction.user, ["id", "username", "discriminator", "tag"]), ), @@ -283,12 +307,13 @@ export class ReacordDiscordJs extends Reacord { user, guild, - reply: (content?: ReactNode) => + reply: (content?: ReactNode, options?: ComponentEventReplyOptions) => this.createInstance( - this.createInteractionReplyRenderer(interaction), + this.createInteractionReplyRenderer(interaction, options ?? {}), content, ), + /** @deprecated Use event.reply(content, { ephemeral: true }) */ ephemeralReply: (content: ReactNode) => this.createInstance( this.createInteractionReplyRenderer(interaction, { diff --git a/packages/reacord/library/core/reacord.tsx b/packages/reacord/library/core/reacord.tsx index 02eba7e..9ff5a52 100644 --- a/packages/reacord/library/core/reacord.tsx +++ b/packages/reacord/library/core/reacord.tsx @@ -23,10 +23,6 @@ export abstract class Reacord { constructor(private readonly config: ReacordConfig = {}) {} - abstract send(...args: unknown[]): ReacordInstance - abstract reply(...args: unknown[]): ReacordInstance - abstract ephemeralReply(...args: unknown[]): ReacordInstance - protected handleComponentInteraction(interaction: ComponentInteraction) { for (const renderer of this.renderers) { if (renderer.handleComponentInteraction(interaction)) return @@ -61,6 +57,7 @@ export abstract class Reacord { {content}, container, ) + return instance }, deactivate: () => { this.deactivate(renderer) diff --git a/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts b/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts index b1c986c..e521286 100644 --- a/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts +++ b/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts @@ -1,4 +1,3 @@ -import type { Interaction } from "../interaction" import type { Message, MessageOptions } from "../message" import { Renderer } from "./renderer" @@ -6,17 +5,23 @@ import { Renderer } from "./renderer" // so we know whether to call reply() or followUp() const repliedInteractionIds = new Set() +export type InteractionReplyRendererImplementation = { + interactionId: string + reply: (options: MessageOptions) => Promise + followUp: (options: MessageOptions) => Promise +} + export class InteractionReplyRenderer extends Renderer { - constructor(private interaction: Interaction) { + constructor(private implementation: InteractionReplyRendererImplementation) { super() } protected createMessage(options: MessageOptions): Promise { - if (repliedInteractionIds.has(this.interaction.id)) { - return this.interaction.followUp(options) + if (repliedInteractionIds.has(this.implementation.interactionId)) { + return this.implementation.followUp(options) } - repliedInteractionIds.add(this.interaction.id) - return this.interaction.reply(options) + repliedInteractionIds.add(this.implementation.interactionId) + return this.implementation.reply(options) } } diff --git a/packages/reacord/package.json b/packages/reacord/package.json index eb5950b..be3eb71 100644 --- a/packages/reacord/package.json +++ b/packages/reacord/package.json @@ -36,7 +36,7 @@ } }, "scripts": { - "build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --sourcemap --dts --dts-resolve", + "build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node18 --format cjs,esm --sourcemap --dts --dts-resolve", "build-watch": "pnpm build -- --watch", "test": "vitest --coverage --no-watch", "test-dev": "vitest", diff --git a/packages/reacord/scripts/discordjs-manual-test.tsx b/packages/reacord/scripts/discordjs-manual-test.tsx index 3a06058..5bfc1b1 100644 --- a/packages/reacord/scripts/discordjs-manual-test.tsx +++ b/packages/reacord/scripts/discordjs-manual-test.tsx @@ -50,7 +50,7 @@ const createTest = async ( } await createTest("basic", (channel) => { - reacord.send(channel.id, "Hello, world!") + reacord.createChannelMessage(channel).render("Hello, world!") }) await createTest("counter", (channel) => { @@ -73,7 +73,7 @@ await createTest("counter", (channel) => { ) } - reacord.send(channel.id, ) + reacord.createChannelMessage(channel).render() }) await createTest("select", (channel) => { @@ -102,8 +102,7 @@ await createTest("select", (channel) => { ) } - const instance = reacord.send( - channel.id, + const instance = reacord.createChannelMessage(channel).render( { instance.render(`you chose ${value}`) @@ -114,8 +113,7 @@ await createTest("select", (channel) => { }) await createTest("ephemeral button", (channel) => { - reacord.send( - channel.id, + reacord.createChannelMessage(channel).render( <>