diff --git a/build.sh b/build.sh index 27962f8..20fb3e2 100644 --- a/build.sh +++ b/build.sh @@ -16,4 +16,8 @@ bun run build echo Building create-bot package cd ../create -bun run build \ No newline at end of file +bun run build + +echo Running ESLint on all packages +cd ../.. +bun check diff --git a/packages/core/src/factories/interaction.ts b/packages/core/src/factories/interaction.ts index 27656cb..6ac3c31 100644 --- a/packages/core/src/factories/interaction.ts +++ b/packages/core/src/factories/interaction.ts @@ -40,7 +40,7 @@ function interactionDataFactory(interaction: InteractionStructure): InteractionD return undefined; case InteractionType.APPLICATION_COMMAND: case InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE: { - return new ApplicationCommandData(interaction.data); + return new ApplicationCommandData(interaction.data) as never; } case InteractionType.MESSAGE_COMPONENT: { return new MessageComponentData(interaction.data); @@ -51,11 +51,9 @@ function interactionDataFactory(interaction: InteractionStructure): InteractionD } } -export type InteractionData = ApplicationCommandData | AutocompleteData | MessageComponentData | ModalSubmitData | undefined; +export type InteractionData = ApplicationCommandData | AutocompleteData | MessageComponentData | ModalSubmitData | undefined; -export interface AutocompleteData extends Omit { - options: ApplicationCommandOptions; -} +export interface AutocompleteData extends ApplicationCommandData {} export interface InteractionReplyOptions extends ReplyOptions { ephemeral?: boolean; @@ -238,7 +236,7 @@ export class Interaction> { + public isApplicationCommandInteraction(): this is Interaction { return this.type === InteractionType.APPLICATION_COMMAND; } @@ -265,7 +263,7 @@ export class Interaction extends Interaction { isPingInteraction: () => this is GuildInteraction; - isApplicationCommandInteraction: () => this is GuildInteraction>; + isApplicationCommandInteraction: () => this is GuildInteraction; isAutocompleteInteraction: () => this is GuildInteraction; isMessageComponentInteraction: () => this is GuildInteraction; isModalSubmitInteraction: () => this is GuildInteraction; @@ -298,7 +296,7 @@ export class GuildInteraction extends Interaction { isPingInteraction: () => this is DMInteraction; - isApplicationCommandInteraction: () => this is DMInteraction>; + isApplicationCommandInteraction: () => this is DMInteraction; isAutocompleteInteraction: () => this is DMInteraction; isMessageComponentInteraction: () => this is DMInteraction; isModalSubmitInteraction: () => this is DMInteraction; @@ -319,43 +317,22 @@ export class DMInteraction extends ApplicationCommandData { +interface GuildApplicationCommandData extends ApplicationCommandData { readonly guildId: string; } -interface UIApplicationCommandData extends ApplicationCommandData { +interface UIApplicationCommandData extends ApplicationCommandData { readonly targetId: string; } -export class ApplicationCommandData { +export class ApplicationCommandData { public readonly id: string; public readonly name: string; public readonly type: ApplicationCommandType; public readonly resolved?: ResolvedDataStructure; - public readonly options: ApplicationCommandOptions; public readonly guildId?: string; public readonly targetId?: string; - public constructor(data: ApplicationCommandDataStructure) { - this.id = data.id; - this.name = data.name; - this.type = data.type; - this.resolved = data.resolved; - this.options = new ApplicationCommandOptions(data.options); - this.guildId = data.guild_id; - this.targetId = data.target_id; - } - - public isGuildApplicationCommand(): this is GuildApplicationCommandData { - return typeof this.guildId !== "undefined"; - } - - public isUIApplicationCommand(): this is UIApplicationCommandData { - return typeof this.targetId !== "undefined"; - } -} - -class ApplicationCommandOptions { readonly #stringOptions = new Map(); readonly #numberOptions = new Map(); readonly #integerOptions = new Map(); @@ -364,13 +341,21 @@ class ApplicationCommandOptions { readonly #channelOptions = new Map(); readonly #roleOptions = new Map(); readonly #mentionableOptions = new Map(); + readonly #attachmentOptions = new Map(); - #focused!: T; + #focused!: FocusedOption; #subCommandGroup: string | undefined; #subCommand: string | undefined; - public constructor(options: ApplicationCommandDataStructure["options"]) { - this.#parseOptions(options); + public constructor(data: ApplicationCommandDataStructure) { + this.id = data.id; + this.name = data.name; + this.type = data.type; + this.resolved = data.resolved; + this.guildId = data.guild_id; + this.targetId = data.target_id; + + this.#parseOptions(data.options); } #parseOptions(options: ApplicationCommandDataStructure["options"]): void { @@ -380,7 +365,8 @@ class ApplicationCommandOptions { const option = options[i]; if (option.focused) { - this.#focused = option.value; + if (typeof option.value === "undefined") continue; + this.#focused = { name: option.name, value: option.value }; continue; } @@ -397,69 +383,75 @@ class ApplicationCommandOptions { } case ApplicationCommandOptionType.STRING: { - if (typeof option.value !== "string") - throw new Error("Something unexpected happened"); + if (typeof option.value !== "string") throw new Error("Something unexpected happened"); this.#stringOptions.set(option.name, option.value); break; } case ApplicationCommandOptionType.INTEGER: { - if (typeof option.value !== "number") - throw new Error("Something unexpected happened"); + if (typeof option.value !== "number") throw new Error("Something unexpected happened"); this.#integerOptions.set(option.name, option.value); break; } case ApplicationCommandOptionType.NUMBER: { - if (typeof option.value !== "number") - throw new Error("Something unexpected happened"); + if (typeof option.value !== "number") throw new Error("Something unexpected happened"); this.#numberOptions.set(option.name, option.value); break; } case ApplicationCommandOptionType.BOOLEAN: { - if (typeof option.value !== "boolean") - throw new Error("Something unexpected happened"); + if (typeof option.value !== "boolean") throw new Error("Something unexpected happened"); this.#booleanOptions.set(option.name, option.value); break; } case ApplicationCommandOptionType.USER: { - if (typeof option.value !== "string") - throw new Error("Something unexpected happened"); + if (typeof option.value !== "string") throw new Error("Something unexpected happened"); this.#userOptions.set(option.name, option.value); break; } case ApplicationCommandOptionType.CHANNEL: { - if (typeof option.value !== "string") - throw new Error("Something unexpected happened"); + if (typeof option.value !== "string") throw new Error("Something unexpected happened"); this.#channelOptions.set(option.name, option.value); break; } case ApplicationCommandOptionType.ROLE: { - if (typeof option.value !== "string") - throw new Error("Something unexpected happened"); + if (typeof option.value !== "string") throw new Error("Something unexpected happened"); this.#roleOptions.set(option.name, option.value); break; } case ApplicationCommandOptionType.MENTIONABLE: { - if (typeof option.value !== "string") - throw new Error("Something unexpected happened"); + if (typeof option.value !== "string") throw new Error("Something unexpected happened"); this.#mentionableOptions.set(option.name, option.value); break; } - case ApplicationCommandOptionType.ATTACHMENT: + case ApplicationCommandOptionType.ATTACHMENT: { + if (typeof this.resolved?.attachments === "undefined") throw new Error("Something unexpected happened"); + if (typeof option.value !== "string") throw new Error("Something unexpected happened"); + + this.#attachmentOptions.set(option.name, this.resolved.attachments[option.value].url); + break; + } } } } - public get focused(): T { - return this.#focused; + public isGuildApplicationCommand(): this is GuildApplicationCommandData { + return typeof this.guildId !== "undefined"; + } + + public isUIApplicationCommand(): this is UIApplicationCommandData { + return typeof this.targetId !== "undefined"; + } + + public getFocused(): ParseFocusedReturnType { + return this.#focused as never; } public get subCommand(): string | undefined { @@ -541,8 +533,24 @@ class ApplicationCommandOptions { return this.#mentionableOptions.get(name); } + + public getAttachment(name: string): string | undefined; + public getAttachment(name: string, assert: true): string; + public getAttachment(name: string, assert = false): string | undefined { + if (assert) + if (!this.#attachmentOptions.has(name)) throw new NotFoundError("Attachment"); + + return this.#attachmentOptions.get(name); + } } +interface FocusedOption { + name: string; + value: T; +} + +type ParseFocusedReturnType = T extends undefined ? undefined : FocusedOption; + class NotFoundError extends Error { public constructor(type: string) { super(); diff --git a/packages/core/src/rest/rest.ts b/packages/core/src/rest/rest.ts index dcf4e06..bd32cbe 100644 --- a/packages/core/src/rest/rest.ts +++ b/packages/core/src/rest/rest.ts @@ -138,7 +138,7 @@ export class REST { return this.#makeRequest("GET", `applications/${clientId}/commands?with_localizations=${withLocalizations}`); } - public async createGlobalApplicationCommands(clientId: string, body: POSTApplicationCommandStructure): Promise { + public async createGlobalApplicationCommand(clientId: string, body: POSTApplicationCommandStructure): Promise { return this.#makeRequest("POST", `applications/${clientId}/commands`, body); } diff --git a/packages/create/.eslintrc.json b/packages/create/.eslintrc.json index b433e89..f81750e 100644 --- a/packages/create/.eslintrc.json +++ b/packages/create/.eslintrc.json @@ -1,6 +1,7 @@ { "ignorePatterns": [ - "src/index.ts" + "src/index.ts", + "src/index-template.ts" ], "extends": [ "../../.eslintrc.json" diff --git a/packages/create/tsconfig.json b/packages/create/tsconfig.json index 4b3d24b..8105ef7 100644 --- a/packages/create/tsconfig.json +++ b/packages/create/tsconfig.json @@ -9,5 +9,8 @@ }, "include": [ "src/**/*" + ], + "exclude": [ + "src/index-template.ts" ] } \ No newline at end of file diff --git a/packages/docs/astro.config.ts b/packages/docs/astro.config.ts index c720a0b..322ad5a 100644 --- a/packages/docs/astro.config.ts +++ b/packages/docs/astro.config.ts @@ -25,18 +25,100 @@ export default defineConfig({ tag: "meta", attrs: { name: "theme-color", - content: "#ff00ef" + content: "#f49ac2" } } ], sidebar: [ + { + label: "Philosophy", + link: "philosophy" + }, { label: "Guides", - autogenerate: { directory: "guides" } + items: [ + { + label: "Getting Started", + link: "/guides/getting-started" + }, + { + label: "Manual Setup", + link: "/guides/manual-setup" + }, + { + label: "Registering Commands", + link: "/guides/registering-commands" + }, + { + label: "Receiving Commands", + link: "/guides/receiving-commands" + } + ] }, { - label: "Reference", - autogenerate: { directory: "reference" } + label: "Documentation", + items: [ + { + label: "JSX Components", + collapsed: true, + badge: { + text: "Beta", + variant: "danger" + }, + items: [ + { + label: "Configuring JSX", + link: "/docs/jsx/configuring-jsx" + }, + { + label: "Embeds", + link: "docs/jsx/embed" + }, + { + label: "Application Commands", + link: "docs/jsx/command" + }, + { + label: "Message Components", + link: "docs/jsx/components" + }, + { + label: "Attachments", + link: "docs/jsx/attachment" + } + ] + }, + { + label: "Handlers", + collapsed: true, + badge: { + text: "Beta", + variant: "danger" + }, + items: [ + { + label: "API", + link: "docs/handlers/the-api", + badge: { + text: "Internals", + variant: "success" + } + }, + { + label: "Handling Application Commands", + link: "docs/handlers/application-commands" + }, + { + label: "Handling Events", + link: "docs/handlers/events" + }, + { + label: "Handling Message Commands", + link: "docs/handlers/message-commands" + } + ] + } + ] } ] }) diff --git a/packages/docs/src/content/config.ts b/packages/docs/src/content/config.ts index 4975aeb..1c3f410 100644 --- a/packages/docs/src/content/config.ts +++ b/packages/docs/src/content/config.ts @@ -1,7 +1,7 @@ import { defineCollection } from "astro:content"; import { docsSchema, i18nSchema } from "@astrojs/starlight/schema"; -export const collections = { +export const collections = { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call docs: defineCollection({ schema: docsSchema() }), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call diff --git a/packages/docs/src/content/docs/docs/handlers/application-commands.md b/packages/docs/src/content/docs/docs/handlers/application-commands.md new file mode 100644 index 0000000..61a1152 --- /dev/null +++ b/packages/docs/src/content/docs/docs/handlers/application-commands.md @@ -0,0 +1,55 @@ +--- +title: Handling Application Commands +description: How to use lilybird's handlers for application commands. +--- + +Currently the `@lilybird/handlers` package provides only one way of handling application commands however i can assure you there are more to come. + +To be completely honest, the current api is not the greatest but was the fastest one for a demo. + +## Creating a simple command + +Lets create a simple `ping` command to show how it works. + +:::caution[Important] +While in the example we are using `@lilybird/jsx` to create the command data you cont need to use it, you can use a normal object and if you are using typescript you will have intellisense +::: + +```diff lang="ts" title="index.ts" +import { createClient, Intents } from "lilybird"; ++import { createHandler } from "@lilybird/handlers"; + ++const listeners = await createHandler({ ++ dirs: { ++ slashCommands: `${import.meta.dir}/commands`, ++ } ++}) + +await createClient({ + token: process.env.TOKEN, + intents: [Intents.GUILDS], +- listeners: {/* your listeners */} ++ ...listeners +}) +``` + +```tsx title="commands/ping.tsx" +import { ApplicationCommand } from "@lilybird/jsx"; +import { SlashCommand } from "@lilybird/handlers"; + +export default { + post: "GLOBAL", + data: , + run: async (interaction) => { + const { ws, rest } = await interaction.client.ping(); + + await interaction.editReply({ + content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\`` + }); + }, +} satisfies SlashCommand +``` + +:::note +The above code was taken from the [bun discord bot](https://github.com/xHyroM/bun-discord-bot), join the bun discord to see it working. +::: \ No newline at end of file diff --git a/packages/docs/src/content/docs/docs/handlers/events.md b/packages/docs/src/content/docs/docs/handlers/events.md new file mode 100644 index 0000000..b9605a9 --- /dev/null +++ b/packages/docs/src/content/docs/docs/handlers/events.md @@ -0,0 +1,39 @@ +--- +title: Handling Events +description: How to use lilybird's handlers for events. +--- + +Currently the `@lilybird/handlers` package provides only one way of handling events however i can assure you there are more to come. + +## Creating a listener + +```diff lang="ts" title="index.ts" +import { createClient, Intents } from "lilybird"; ++import { createHandler } from "@lilybird/handlers"; + ++const listeners = await createHandler({ ++ dirs: { ++ events: `${import.meta.dir}/events`, ++ } ++}) + +await createClient({ + token: process.env.TOKEN, + intents: [Intents.GUILDS], +- listeners: {/* your listeners */} ++ ...listeners +}) +``` + +```ts title="events/ping.ts" +import { Event } from "@lilybird/handlers"; + +export default { + event: "ready", + run: (client) => { + console.log(`Logged in as ${client.user.username}`); + }, +// This duplication is needed for typescript types to work properly +// This is also why this api isn't the best +} satisfies Event<"ready"> +``` \ No newline at end of file diff --git a/packages/docs/src/content/docs/docs/handlers/message-commands.md b/packages/docs/src/content/docs/docs/handlers/message-commands.md new file mode 100644 index 0000000..d604457 --- /dev/null +++ b/packages/docs/src/content/docs/docs/handlers/message-commands.md @@ -0,0 +1,57 @@ +--- +title: Handling Message Commands +description: How to use lilybird's handlers for application commands. +--- + +Currently the `@lilybird/handlers` package provides only one way of handling application commands however i can assure you there are more to come. + +## Creating a simple command + +Lets create a simple `ping` command to show how it works. + +```diff lang="ts" title="index.ts" +import { createClient, Intents } from "lilybird"; ++import { createHandler } from "@lilybird/handlers"; + ++const listeners = await createHandler({ ++ dirs: { ++ messageCommands: `${import.meta.dir}/commands`, ++ } ++}) + +await createClient({ + token: process.env.TOKEN, + intents: [Intents.GUILDS], +- listeners: {/* your listeners */} ++ ...listeners +}) +``` + +:::note +The second argument `args` is the result of running the following code: +```ts +message.content.slice(this.prefix.length) + .trim() + .split(/\s+/g) + .shift() +``` +::: + +```ts title="commands/ping.ts" +import { MessageCommand } from "@lilybird/handlers"; + +export default { + name: "ping", + run: async (message, args) => { + const { ws, rest } = await message.client.ping(); + + await message.edit({ + content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\`` + }); + }, +} satisfies MessageCommand +``` + +:::note +The above code was taken from the [bun discord bot](https://github.com/xHyroM/bun-discord-bot), join the bun discord to see it working. +::: \ No newline at end of file diff --git a/packages/docs/src/content/docs/docs/handlers/the-api.md b/packages/docs/src/content/docs/docs/handlers/the-api.md new file mode 100644 index 0000000..5e2bae5 --- /dev/null +++ b/packages/docs/src/content/docs/docs/handlers/the-api.md @@ -0,0 +1,61 @@ +--- +title: The Handler API +description: How lilybird's handler api works. +--- + +The current handler api is fairly simple, you have a class that you can extend that contains the methods that load the files, the ones that load the commands and the function that is responsible for creating the listeners. + +Lets break down the function and helpers used to create the listeners and explain how we are circumventing the limitations ov not having event listeners. + +## Building the listeners + +As we can see highlighted in **gray** in the code bellow we create 2 placeholder functions, this is so you can have your own listener without conflicting with the handler. + +:::caution +You should be careful and avoid at all costs modifying the `interaction` object, since objects are passed by reference if you mutate in your listeners the changes will pass to the command handler. + +While you can use that on your favor we highly advise against it. +::: + +```ts {2, 3} +public buildListeners(): ClientEventListeners { + let interactionCreateFn = function () { return; }; + let messageCreateFn = function () { return; }; + + const listeners: ClientEventListeners = {}; + + for (const [name, event] of this.events) { + if (name === "interactionCreate") { + interactionCreateFn = event.run; + continue; + } + + if (name === "messageCreate") { + messageCreateFn = event.run; + continue; + } + + listeners[name] = event.run; + } + + listeners.interactionCreate = async (interaction) => { + await interactionCreateFn(interaction); + await this.onInteraction(interaction); + }; + + listeners.messageCreate = async (message) => { + await messageCreateFn(message); + await this.onMessage(message); + }; + + return listeners; +} +``` + +## Registering the commands + +Lilybird has a neat api that makes this job trivial. + +The `setup` api is a simple callback that the client runs the moment it connects to discord. In the future this will be change so it gets called even before the connection is called. + +This is why when you call `createHandler` you spread the result into the client options instead of passing the variable. \ No newline at end of file diff --git a/packages/docs/src/content/docs/docs/jsx/attachment.mdx b/packages/docs/src/content/docs/docs/jsx/attachment.mdx new file mode 100644 index 0000000..b1471da --- /dev/null +++ b/packages/docs/src/content/docs/docs/jsx/attachment.mdx @@ -0,0 +1,62 @@ +--- +title: Attachments +description: How to use lilybird's Attachment component. +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; + +## Creating attachments + +The `Attachment` component is really simple to use, it just gets either a file or a path and returns a `LilybirdAttachment`. + +:::caution +Passing in `path` instead of `file` will make it so lilybird will try to use `Bun` apis, so if you are using `node` pass in a `Blob` or extension of blob instead. +::: + + + + ```tsx + import { Attachment } from "@lilybird/jsx"; + import { join } from "node:path" + + const att = ; + ``` + + + ```tsx + import { Attachment } from "@lilybird/jsx"; + import { readFile } from "node:fs"; + import { join } from "node:path" + + const att = ; + ``` + + + +## Using attachments + +When using the the `Attachment` component you can easily pass in `.uri` to embeds and other places that use the uri form of attachments. + +An example would be: + +```tsx ins="att.uri" +import { Attachment, Embed, EmbedImage } from "@lilybird/jsx"; +import { join } from "node:path" + +const att = ; + +const embed = ( + + + +); +``` \ No newline at end of file diff --git a/packages/docs/src/content/docs/docs/jsx/command.md b/packages/docs/src/content/docs/docs/jsx/command.md new file mode 100644 index 0000000..005c1b9 --- /dev/null +++ b/packages/docs/src/content/docs/docs/jsx/command.md @@ -0,0 +1,23 @@ +--- +title: Application Commands +description: How to use lilybird's ApplicationCommand component. +--- + +## Slash Commands + +Creating slash commands with lilybird's jsx components is a trivial task, you can create one with the parent element `ApplicationCommand` and add options by importing the respective `Option`. + +```tsx title="doc-command.tsx" +import { ApplicationCommand, StringOption, UserOption } from "@lilybird/jsx"; + +const command = ( + + + + +); +``` + +:::note +The above code was taken from the [bun discord bot](https://github.com/xHyroM/bun-discord-bot) as they have been a huge help in this project. +::: \ No newline at end of file diff --git a/packages/docs/src/content/docs/docs/jsx/components.md b/packages/docs/src/content/docs/docs/jsx/components.md new file mode 100644 index 0000000..a1fef87 --- /dev/null +++ b/packages/docs/src/content/docs/docs/jsx/components.md @@ -0,0 +1,71 @@ +--- +title: Message Components +description: How to use lilybird's message components. +--- + +The building block of all message components is the `ActionRow` component. +It wraps all the other components and exists exactly just for that reason. + +All of the components have all the properties that discord allows and they are really simple to use. + +Discord reference: [Message Components](https://discord.com/developers/docs/interactions/message-components) + +## Buttons + +Buttons are the simplest and most used component type, lilybird makes their usage even more trivial. + +```tsx +import { ActionRow, Button } from "@lilybird/jsx"; +import { ButtonStyle } from "lilybird"; + +const buttonRow = ( + +