Neptune's Personal Maid

    @jadl/cmd
    TypeScript icon, indicating that this package has built-in type declarations

    0.7.5 • Public • Published

    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

    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 @Command(name: string, description: string)

    Once you've declared your class, you create a method to run the command through, and mark it with a @Run() decorator

    You can return anything that can be parsed by @jadl/builders, like a string, or one of the many builders. Here's an example;

    import { Command } from '@jadl/cmd'
    
    @Command('wave', 'Wave at someone!') // sets the command name to /wave
    export class WaveCommand {
      @Run() // 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

    import { Command, Run, Worker, Author } from '@jadl/cmd'
    import { Embed } from '@jadl/builders' // optional, but used for many different compatible builders!
    
    @Command('hello', 'Say hello!')
    export class HelloCommand {
      @Run()
      hello (
        @Worker() worker: Bot, // creates a paramater that is your worker
        @Author() 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:

    • @Worker, gets the main worker
    • @Interaction gets the interaction object
    • @Guild gets the guild
    • @Author gets the running user
    • @Member gets the running member
    • @Me gets the worker's member in this guild

    (many more to come) // WIP

    Creating options

    The @Options.[] decorator is used to apply options to a command. For example @Options.String('name', 'description', { options })

    e.g let's add a user via the Options.User decorator. This will create a Discord interaction user option

    import { Command, Run, Options } from '@jadl/cmd'
    import { MessageBuilder, Embed } from '@jadl/builders'
    
    @Command('wave', 'Wave at someone!')
    export class WaveCommand {
      @Run()
      wave (
        @Options.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

    import { Command, Permissions, Run } from '@jadl/cmd'
    
    @Command('admin', 'Only users with Manager Server can run this command')
    @Permissions('manageServer') // only Manage Server permissions (overrideable by permissions v2)
    export class AdminCommand {
      @Run()
      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 @Command() 3rd parameter

    And then you can use the @Targets 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!

    import { Command, Run, Targets, Author } from '@jadl/cmd'
    import { Embed } from '@jadl/builders'
    
    import { ApplicationCommandType } from 'discord-api-types/v9'
    
    @Command('Wave at user', undefined, ApplicationCommandType.User)
    export class WaveCommand {
      @Run()
      wave (
        @Targets.User() user: APIUser, // gets the user who the command was ran ON
        @Author() 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 @UserPerms()

    @Command('ban', 'Ban a user')
    class BanCommand {
      @Run()
      @UserPerms('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

    @Command('location', 'Find location')
    export class LocationCommand {
      @Run()
      location (
        @AutoComplete(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
        })
        @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 @Command() 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

    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

    import { UserLocked } from '../decorators/UserLocked'
    
    @Command('wave', 'Wave at someone!')
    export class WaveCommand {
      @Run()
      @UserLocked('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

    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 @Db() decorator shorthand in our method

    import { Db } from '../decorators/Db'
    
    @Command('wave', 'Wave at someone!')
    export class WaveCommand {
      @Run()
      wave (
        @Db() db: GuildSettings, // makes our db parameter
        @Author() 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

    @jadl/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

    import { ButtonMenu, Section } from '@jadl/cmd'
    
    export class HelpMenu extends ButtonMenu {
      @Section({
        // button object
        style: ButtonStyle.Primary,
        label: 'Fun'
      })
      fun () {
        return new Embed()
          .title('Fun')
          .field('/8ball', '8-ball command')
          .field('/fortune', 'Gets your fortune')
      }
    
      @Section({
        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

    // 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

    import { helpMenu } from '../menu/HelpMenu'
    
    @Command('help', 'Get help')
    export class HelpCommand {
      @Run()
      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

    interface BarMenuData {
      foo: string // the selected video
    }
    
    export class BarMenu extends ButtonMenu<BarMenuData> {
      @Section({
        ...
      })
      foo (data: BarMenuData) {
        data.foo = 'abc' // set the data
    
        return 'hello' // respond
      }
    
      @Section({
        ...
      })
      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.

    ⚠️ 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

    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()
      run () {
        return new MessageBuilder()
          .setMessage({ content: 'Click button!!' })
          .addComponentRow(HelloWorldCommand.helloWorldButton) // add the button to a component row
      }
    }

    src/index.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

    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()
      run () {
        return HelloWorldCommand.helloWorldModal.render() // return the modal to any interaction response!
      }
    }

    Install

    npm i @jadl/cmd

    DownloadsWeekly Downloads

    175

    Version

    0.7.5

    License

    Apache-2.0

    Unpacked Size

    91.9 kB

    Total Files

    68

    Last publish

    Collaborators

    • jpbberry