diff --git a/packages/reacord/library/core/file.ts b/packages/reacord/library/core/file.ts new file mode 100644 index 0000000..3dd8ad3 --- /dev/null +++ b/packages/reacord/library/core/file.ts @@ -0,0 +1,7 @@ +import { Readable } from "node:stream" + +export type ReacordFile = { + name?: string + description?: string + data: Buffer | Readable | string +} diff --git a/packages/reacord/library/core/instance.ts b/packages/reacord/library/core/instance.ts index d0aa740..59e020b 100644 --- a/packages/reacord/library/core/instance.ts +++ b/packages/reacord/library/core/instance.ts @@ -1,4 +1,5 @@ import type { ReactNode } from "react" +import { ReacordFile } from "./file" /** * Represents an interactive message, which can later be replaced or deleted. @@ -16,4 +17,7 @@ export type ReacordInstance = { * This prevents it from listening to user interactions. */ deactivate: () => void + + /** Attach a file to the message for this instance */ + attach: (file: ReacordFile) => void } diff --git a/packages/reacord/library/core/reacord-discord-js.ts b/packages/reacord/library/core/reacord-discord-js.ts index 2e9e3a0..386b38a 100644 --- a/packages/reacord/library/core/reacord-discord-js.ts +++ b/packages/reacord/library/core/reacord-discord-js.ts @@ -301,6 +301,15 @@ function createReacordMessage(message: Discord.Message): Message { delete: async () => { await message.delete() }, + updateFiles: async (files) => { + await message.edit({ + files: files.map(({ name, description, data }) => ({ + name, + description, + attachment: data, + })), + }) + }, } } @@ -310,6 +319,10 @@ function createEphemeralReacordMessage(): Message { console.warn("Ephemeral messages can't be edited") return Promise.resolve() }, + updateFiles: () => { + console.warn("Ephemeral messages can't be edited") + return Promise.resolve() + }, delete: () => { console.warn("Ephemeral messages can't be deleted") return Promise.resolve() @@ -358,7 +371,10 @@ function getDiscordMessageOptions( })), } - if (!options.content && !options.embeds?.length) { + const hasContent = + options.content || options.embeds?.length || options.files?.length + + if (!hasContent) { options.content = "_ _" } diff --git a/packages/reacord/library/core/reacord.tsx b/packages/reacord/library/core/reacord.tsx index d402ffe..6a1d9fa 100644 --- a/packages/reacord/library/core/reacord.tsx +++ b/packages/reacord/library/core/reacord.tsx @@ -63,6 +63,9 @@ export abstract class Reacord { this.renderers = this.renderers.filter((it) => it !== renderer) renderer.destroy() }, + attach: (file) => { + renderer.attach(file) + }, } if (initialContent !== undefined) { diff --git a/packages/reacord/library/internal/channel.ts b/packages/reacord/library/internal/channel.ts index b574496..f0a02b0 100644 --- a/packages/reacord/library/internal/channel.ts +++ b/packages/reacord/library/internal/channel.ts @@ -1,5 +1,7 @@ +import { ReacordFile } from "../core/file" import type { Message, MessageOptions } from "./message" export type Channel = { send(message: MessageOptions): Promise + sendFiles(files: readonly ReacordFile[]): Promise } diff --git a/packages/reacord/library/internal/message.ts b/packages/reacord/library/internal/message.ts index 1533844..db2130b 100644 --- a/packages/reacord/library/internal/message.ts +++ b/packages/reacord/library/internal/message.ts @@ -2,6 +2,7 @@ import type { Except } from "type-fest" import { last } from "../../helpers/last" import type { EmbedOptions } from "../core/components/embed-options" import type { SelectProps } from "../core/components/select" +import { ReacordFile } from "../main" export type MessageOptions = { content: string @@ -49,6 +50,7 @@ export type MessageSelectOptionOptions = { export type Message = { edit(options: MessageOptions): Promise delete(): Promise + updateFiles(files: readonly ReacordFile[]): Promise } export function getNextActionRow(options: MessageOptions): ActionRow { diff --git a/packages/reacord/library/internal/renderers/channel-message-renderer.ts b/packages/reacord/library/internal/renderers/channel-message-renderer.ts index 32fafe1..af99920 100644 --- a/packages/reacord/library/internal/renderers/channel-message-renderer.ts +++ b/packages/reacord/library/internal/renderers/channel-message-renderer.ts @@ -1,3 +1,4 @@ +import { ReacordFile } from "../../core/file" import type { Channel } from "../channel" import type { Message, MessageOptions } from "../message" import { Renderer } from "./renderer" @@ -10,4 +11,10 @@ export class ChannelMessageRenderer extends Renderer { protected createMessage(options: MessageOptions): Promise { return this.channel.send(options) } + + protected createMessageFromFiles( + files: readonly ReacordFile[], + ): Promise { + return this.channel.sendFiles(files) + } } diff --git a/packages/reacord/library/internal/renderers/renderer.ts b/packages/reacord/library/internal/renderers/renderer.ts index fb9146d..104d48a 100644 --- a/packages/reacord/library/internal/renderers/renderer.ts +++ b/packages/reacord/library/internal/renderers/renderer.ts @@ -1,5 +1,6 @@ import { Subject } from "rxjs" import { concatMap } from "rxjs/operators" +import { ReacordFile } from "../../core/file" import { Container } from "../container.js" import type { ComponentInteraction } from "../interaction" import type { Message, MessageOptions } from "../message" @@ -7,15 +8,21 @@ import type { Node } from "../node.js" type UpdatePayload = | { action: "update" | "deactivate"; options: MessageOptions } + | { action: "files"; files: readonly ReacordFile[] } | { action: "deferUpdate"; interaction: ComponentInteraction } | { action: "destroy" } +type NewMessagePayload = + | { source: "content"; messageOptions?: MessageOptions } + | { source: "files"; files?: readonly ReacordFile[] } + export abstract class Renderer { readonly nodes = new Container>() private componentInteraction?: ComponentInteraction private message?: Message private active = true private updates = new Subject() + private files: readonly ReacordFile[] = [] private updateSubscription = this.updates .pipe(concatMap((payload) => this.updateMessage(payload))) @@ -46,6 +53,11 @@ export abstract class Renderer { this.updates.next({ action: "destroy" }) } + attach(file: ReacordFile) { + const newFiles = (this.files = [...this.files, file]) + this.updates.next({ action: "files", files: newFiles }) + } + handleComponentInteraction(interaction: ComponentInteraction) { this.componentInteraction = interaction @@ -60,7 +72,7 @@ export abstract class Renderer { } } - protected abstract createMessage(options: MessageOptions): Promise + protected abstract createMessage(options: NewMessagePayload): Promise private getMessageOptions(): MessageOptions { const options: MessageOptions = { @@ -102,6 +114,15 @@ export abstract class Renderer { return } + if (payload.action === "files") { + if (this.message) { + await this.message?.updateFiles(payload.files) + } else { + this.message = await this.createMessageFromFiles(payload.files) + } + return + } + if (this.componentInteraction) { const promise = this.componentInteraction.update(payload.options) this.componentInteraction = undefined diff --git a/packages/reacord/library/main.ts b/packages/reacord/library/main.ts index 8ae77f1..755f8ab 100644 --- a/packages/reacord/library/main.ts +++ b/packages/reacord/library/main.ts @@ -12,6 +12,7 @@ export * from "./core/components/embed-title" export * from "./core/components/link" export * from "./core/components/option" export * from "./core/components/select" +export * from "./core/file" export * from "./core/instance" export { useInstance } from "./core/instance-context" export * from "./core/reacord" diff --git a/packages/reacord/playground/anime.jpg b/packages/reacord/playground/anime.jpg new file mode 100644 index 0000000..3fd1654 Binary files /dev/null and b/packages/reacord/playground/anime.jpg differ diff --git a/packages/reacord/playground/command-handler.ts b/packages/reacord/playground/command-handler.ts index 2b49ad0..9d93e74 100644 --- a/packages/reacord/playground/command-handler.ts +++ b/packages/reacord/playground/command-handler.ts @@ -8,16 +8,14 @@ type Command = { export function createCommandHandler(client: Client, commands: Command[]) { client.on("ready", async () => { - for (const command of commands) { - for (const guild of client.guilds.cache.values()) { - await client.application?.commands.create( - { - name: command.name, - description: command.description, - }, - guild.id, - ) - } + for (const guild of client.guilds.cache.values()) { + client.application!.commands.set( + commands.map(({ name, description }) => ({ + name, + description, + })), + guild.id, + ) } }) diff --git a/packages/reacord/playground/main.tsx b/packages/reacord/playground/main.tsx index 2e3492d..a1c9e6d 100644 --- a/packages/reacord/playground/main.tsx +++ b/packages/reacord/playground/main.tsx @@ -1,6 +1,9 @@ import { Client } from "discord.js" import "dotenv/config" +import { readFile } from "fs/promises" +import { join } from "path" import React from "react" +import { fileURLToPath } from "url" import { Button, ReacordDiscordJs, useInstance } from "../library/main" import { createCommandHandler } from "./command-handler" import { Counter } from "./counter" @@ -104,6 +107,19 @@ createCommandHandler(client, [ reacord.reply(interaction, ) }, }, + { + name: "anime", + description: "shows an anime image", + run: async (interaction) => { + const reply = reacord.reply(interaction) + const image = await readFile( + join(fileURLToPath(import.meta.url), "../anime.jpg"), + ) + + reply.attach({ name: "anime.jpg", data: image }) + // reply.render("anime") + }, + }, ]) await client.login(process.env.TEST_BOT_TOKEN)