@jadl/cmd
Version:
A command handler for JADL
491 lines (378 loc) • 14.8 kB
Markdown
# JADL Command Handler
## This new version of the command handler is WIP and not stable, use with caution.
# Prerequisites
If you're going to use the decorators built into the command handler, make sure to set `"experimentalDecorators": true` in your tsconfig.json
## Creating a Command Handler
Create a command handler with the CommandHandler class
```ts
import { CommandHandler } from '@jadl/cmd'
import { WaveCommand } from './commands/WaveCommand' // example command
const commands = new CommandHandler(worker, [
// your command classes
WaveCommand
])
```
## Creating a command
Commands are classes that will be filled with decorator to dictate their functionality
Our main decorator for a command is `(name: string, description: string)`
Once you've declared your class, you create a method to run the command through, and mark it with a `()` decorator
You can return anything that can be parsed by [/builders](https://npmjs.com//builders), like a string, or one of the many builders. Here's an example;
```ts
import { Command } from '@jadl/cmd'
('wave', 'Wave at someone!') // sets the command name to /wave
export class WaveCommand {
() // marks this method as the main running command
wave () {
return 'Hello!'
}
}
```
Simplest command in the book. Lets get a bit more advanced.
### Accessing data with parameter decorators
Using parameter decorators is how we access all of our important data, to make your command a real command
Many of them exist and allow for adding and using interaction options, or just accessing your normal data objects
e.g let's add a user via the `Options.User` decorator. This will create a Discord interaction user option
```ts
import { Command, Run, Worker, Author } from '@jadl/cmd'
import { Embed } from '@jadl/builders' // optional, but used for many different compatible builders!
('hello', 'Say hello!')
export class HelloCommand {
()
hello (
() worker: Bot, // creates a paramater that is your worker
() author: APIUser // gets the user who sent the command
) {
// you can now use this parameter as it's actual value! making it super easy to do what you need to do
return new Embed()
.description(`Hey <@${author.id}>! Thanks for saying hi! My name is ${worker.user.username}!`)
}
}
// note, discord-api-types is EXTREMELY useful and will let you add types for all of these
```
Some helpful decorators:
- ``, gets the main worker
- `` gets the interaction object
- `` gets the guild
- `` gets the running user
- `` gets the running member
- `` gets the worker's member in this guild
(many more to come) // WIP
### Creating options
The `.[]` decorator is used to apply options to a command. For example `.String('name', 'description', { options })`
e.g let's add a user via the `Options.User` decorator. This will create a Discord interaction user option
```ts
import { Command, Run, Options } from '@jadl/cmd'
import { MessageBuilder, Embed } from '@jadl/builders'
('wave', 'Wave at someone!')
export class WaveCommand {
()
wave (
.User('user', 'User to wave at', {
required: true
}) user: APIUser // creates an option accepting type user
) {
// you can now use this parameter as it's actual value! making it super easy to do what you need to do
return new MessageBuilder({ content: `Hey <@${user.id}>`})
.addEmbed(
new Embed()
.description('Someone waved at you')
)
}
}
```
### Extra command decorators
#### Permissions
For permissions v2 there is a `Permissions(permission)` decorator
```ts
import { Command, Permissions, Run } from '@jadl/cmd'
('admin', 'Only users with Manager Server can run this command')
('manageServer') // only Manage Server permissions (overrideable by permissions v2)
export class AdminCommand {
()
run () {
superSecretThing() // only people who are allowed to run the command in the server can do it
}
}
```
### Targets & message/user commands
You can create a user or message command by passing the type of command you'd like to the `()` 3rd parameter
And then you can use the `` decorator as a value for those targets. Be careful to make sure to use the correct target for your command
e.g lets make our wave command a user command!
```ts
import { Command, Run, Targets, Author } from '@jadl/cmd'
import { Embed } from '@jadl/builders'
import { ApplicationCommandType } from 'discord-api-types/v9'
('Wave at user', undefined, ApplicationCommandType.User)
export class WaveCommand {
()
wave (
.User() user: APIUser, // gets the user who the command was ran ON
() author: APIUser // gets the user who the command was ran FROM
) {
return new Embed()
.description(`Hey <@${user.id}>! <@${author.id}> waved at you!`)
}
}
```
### Middleware / interceptor decorators
With decorators it's super easy to create and use decorator for specific commands. There's a few built in. For example `()`
```ts
('ban', 'Ban a user')
class BanCommand {
()
('administrator') // this will require the user has the administrator permission
run () {
// run ban
}
}
```
### AutoComplete
You can add an autocomplete method to your parameters with the `AutoComplete` decorator
```ts
('location', 'Find location')
export class LocationCommand {
()
location (
(async (term) => { // make sure autocomplete is ABOVE option def
const search = await searcher.search(term) // term is the search term
return search.map(x => { name: x.name, value: x.name }) // return options
})
.String('location', 'Location to find')
location: string
) {
...
}
}
```
## Creating your own
You can also make your own decorators with the `Decorators` object
There are 3 types of decorators
### Base Decorators
Base decorators go above the command class similar to the `()` decorator. These are mostly used for internal things, but it's available
You can create this with the `Decorators.createBaseDecorator()` method
### Command Decorators
Command decorators are the decorators that go over the method being ran, and are generally used as middleware / interceptors, or just defining extra metadata about the command
You can create these with the `Decorators.createCommandDecorator()` method
e.g let's create a specific user only decorator
**src/decorators/UserLocked.ts**
```ts
import { Decorators } from '@jadl/cmd' // global utility for everything decorators
export const UserLocked = Decorators.createCommandDecorator<[
// options
userId: string
]>(([userId], cmd) => {
// Use the canRun array of functions
cmd.canRun.push((interaction, handler) => { // interaction being the raw object
// return a boolean of whether or not the author is the user locked
return interaction.user.id === userId
})
// you can also use the onRun array of functions to disregard returning and errors
})
```
You can now use this in your command like so
```ts
import { UserLocked } from '../decorators/UserLocked'
('wave', 'Wave at someone!')
export class WaveCommand {
()
('277183033344524288') // this will now apply the above canRun method
run () {
return 'Wave!' // only ran if the user matches the user locked to this command
}
}
```
### Parameter decorators
Parameter decorators are used to pass data to the running method when a command has been ran
The essential way this works is that you're giving the command handler a function that will be ran and positioned to your method based on the parameter
You can create these with the `Decorators.createParameterDecorator()` method
e.g let's make a database decorator
**src/decorators/Db.ts**
```ts
import { Decorators } from '@jadl/cmd'
// you can add options the same was as was done above, however we don't need that here
export const Db = Decorators.createParameterDecorator((options) => {
return async (interaction, { worker }) => { // this method is ran EVERYTIME a command is ran, and it's return value is what shows up on the parameter for your method
return await worker.db.guildSettings.get(interaction.guild_id) // returns the guild's database
}
})
```
Now we can use the `()` decorator shorthand in our method
```ts
import { Db } from '../decorators/Db'
('wave', 'Wave at someone!')
export class WaveCommand {
()
wave (
() db: GuildSettings, // makes our db parameter
() author: APIUser // and you can add as many of these params as you'd like!
) {
// and now db will be whatever was returned in the Db decorator!
if (!db.users.includes(author.id)) return "You can't do that!"
return 'Wave!'
}
}
```
## Extras
`/cmd` comes with some pre-built extra interaction helpers, not necesarilly just do with commands
### ButtonMenu
Creates an easy to understand button navigation menu
Say we want to create a categorized help command
**src/menus/HelpMenu.ts**
```ts
import { ButtonMenu, Section } from '@jadl/cmd'
export class HelpMenu extends ButtonMenu {
({
// button object
style: ButtonStyle.Primary,
label: 'Fun'
})
fun () {
return new Embed()
.title('Fun')
.field('/8ball', '8-ball command')
.field('/fortune', 'Gets your fortune')
}
({
style: ButtonStyle.Primary,
label: 'Admin'
})
admin () {
return new Embed()
.title('Admin')
.field('/ban', 'Bans user')
.field('/kick', 'Kicks user')
}
}
export const helpMenu = new HelpMenu() // instantiate your menu
```
**src/index.ts**
```ts
// you need to import the menu as a WorkerInject into the command handler
new CommandHandler(
worker,
[
HelpCommand,
// ... your commands
helpMenu // the instantiated help menu
// ... whatever else
]
)
```
**src/commands/HelpCommand**
```ts
import { helpMenu } from '../menu/HelpMenu'
('help', 'Get help')
export class HelpCommand {
()
help () {
return helpMenu.start(
'fun' // the method name where you want to start
)
// .start() returns a MessageBuilder
}
}
```
This will just return a menu, with two buttons, where when you click on the corresponding button, the embed for that section will show up!
#### Stateless design
ButtonMenu's can hold stateless values within the button's `custom_id`. All you need to do is use the first parameter of every function, which is a mutable object that holds the data, and will update on response. E.g
```ts
interface BarMenuData {
foo: string // the selected video
}
export class BarMenu extends ButtonMenu<BarMenuData> {
({
...
})
foo (data: BarMenuData) {
data.foo = 'abc' // set the data
return 'hello' // respond
}
({
...
})
bar (data: BarMenuData) {
data.foo // will be 'abc' if foo was clicked first
// change or manipulate or use
...
}
}
// when starting the menu you must instantiate with initial data as well
barMenu.start('foo', { foo: 'def' })
```
Because it holds all of this data statelessly, it will even maintain it's data over restarts.
> :warning: There is a limitation on the length of the `custom_id`, it is not recommended to store information entered by a user / not maintainably short. Data is held in a URLSearchParams format.
### ComponentRunner
ComponentRunner allows you to create a button + handler within a single class, and feed it to a MessageBuilder, making code more seamless and easy to comprehend.
**src/commands/HelloWorld.ts**
```ts
import { ComponentRunner } from '@jadl/cmd'
import { MessageBuilder } from '@jadl/builders'
export class HelloWorldCommand {
// static to make it easier to import
static helloWorldButton = new ComponentRunner({
// component object
type: ComponentType.Button // works with select menu too
style: ButtonStyle.Primary,
custom_id: 'hello_world_button', // Make sure this is unique!
label: 'Click me'
})
.setHandle((int, worker) => {
// handle the interaction
return new Embed() // return anything parseable
.title('You click it good job.')
})
()
run () {
return new MessageBuilder()
.setMessage({ content: 'Click button!!' })
.addComponentRow(HelloWorldCommand.helloWorldButton) // add the button to a component row
}
}
```
**src/index.ts**
```ts
// you need to import the button as a WorkerInject into the command handler
new CommandHandler(
worker,
[
HelloWorldCommand,
// ... your commands
HelloWorldCommand.helloWorldButton // the button
// ... whatever else
]
)
```
And your message will now have a button, that when pressed will return your new embed. Much nicer to organize!
### ModalRunner
ModalRunner allows for a similar experience as ComponentRunner, except with modals
**src/commands/HelloWorld.ts**
```ts
import { ComponentRunner } from '@jadl/cmd'
import { MessageBuilder } from '@jadl/builders'
export class HelloWorldCommand {
// static to make it easier to import
static helloWorldModal = new ModalRunner('Hello World!') // enter the title in the constructor
.addTextInput('Foo Bar', { // add a label for the component
// now add any extra props, the only required one is "style"
style: TextInputStyle.Paragraph
})
.addTextInput('Welcome', { // add multiple in a builder format
style: TextInputStyle.Short,
placeholder: 'Put some stuff in here!'
})
.setHandle((options, worker, interaction) => {
// get all of your info here
// options is a cool object that will be an object mapped by the label name to it's value
console.log(options['Foo Bar'])
// our object will look something like { 'Foo Bar': '...', 'Welcome': '...' }
// and you can return a message
return new MessageBuilder()
.setMessage({ content: 'got your submission!' })
// or return nothing for no response
})
()
run () {
return HelloWorldCommand.helloWorldModal.render() // return the modal to any interaction response!
}
}
```