mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-12-11 07:54:58 +00:00
diamondburned
273fcf1418
This commit adds subcommand aliases as well as additional code in HelpGenerate to cover for both subcommand and command aliases. A breaking change is that {,Must}RegisterSubcommandCustom methods are now replaced with normal {,Must}RegisterSubcommand methods. This is because they now use variadic strings, which could take 0, 1 or more arguments. This commit also allows AddMiddleware and similar methods to be given a method directly: sub.Plumb(cmds.PlumbedHandler) sub.AddMiddleware(cmds.PlumbedHandler, cmds.plumbMiddleware) This change closes issue #146.
531 lines
14 KiB
Go
531 lines
14 KiB
Go
package bot
|
|
|
|
import (
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/diamondburned/arikawa/v2/gateway"
|
|
)
|
|
|
|
var (
|
|
typeMessageCreate = reflect.TypeOf((*gateway.MessageCreateEvent)(nil))
|
|
typeMessageUpdate = reflect.TypeOf((*gateway.MessageUpdateEvent)(nil))
|
|
|
|
typeIError = reflect.TypeOf((*error)(nil)).Elem()
|
|
typeIManP = reflect.TypeOf((*ManualParser)(nil)).Elem()
|
|
typeICusP = reflect.TypeOf((*CustomParser)(nil)).Elem()
|
|
typeIParser = reflect.TypeOf((*Parser)(nil)).Elem()
|
|
typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem()
|
|
typeSetupFn = methodType((*CanSetup)(nil), "Setup")
|
|
)
|
|
|
|
func methodType(iface interface{}, name string) reflect.Type {
|
|
method, _ := reflect.TypeOf(iface).
|
|
Elem().
|
|
MethodByName(name)
|
|
return method.Type
|
|
}
|
|
|
|
// HelpUnderline formats command arguments with an underline, similar to
|
|
// manpages.
|
|
var HelpUnderline = true
|
|
|
|
func underline(word string) string {
|
|
if HelpUnderline {
|
|
return "__" + word + "__"
|
|
}
|
|
return word
|
|
}
|
|
|
|
// Subcommand is any form of command, which could be a top-level command or a
|
|
// subcommand.
|
|
//
|
|
// Allowed method signatures
|
|
//
|
|
// These are the acceptable function signatures that would be parsed as commands
|
|
// or events. A return type <T> implies that return value will be ignored.
|
|
//
|
|
// func(*gateway.MessageCreateEvent, ...) (string, error)
|
|
// func(*gateway.MessageCreateEvent, ...) (*discord.Embed, error)
|
|
// func(*gateway.MessageCreateEvent, ...) (*api.SendMessageData, error)
|
|
// func(*gateway.MessageCreateEvent, ...) (T, error)
|
|
// func(*gateway.MessageCreateEvent, ...) error
|
|
// func(*gateway.MessageCreateEvent, ...)
|
|
// func(<AnyEvent>) (T, error)
|
|
// func(<AnyEvent>) error
|
|
// func(<AnyEvent>)
|
|
//
|
|
type Subcommand struct {
|
|
// Description is a string that's appended after the subcommand name in
|
|
// (*Context).Help().
|
|
Description string
|
|
|
|
// Hidden if true will not be shown by (*Context).Help(). It will
|
|
// also cause unknown command errors to be suppressed.
|
|
Hidden bool
|
|
|
|
// Raw struct name, including the flag (only filled for actual subcommands,
|
|
// will be empty for Context):
|
|
StructName string
|
|
// Parsed command name:
|
|
Command string
|
|
|
|
// Aliases is alternative way to call this subcommand in Discord.
|
|
Aliases []string
|
|
|
|
// SanitizeMessage is executed on the message content if the method returns
|
|
// a string content or a SendMessageData.
|
|
SanitizeMessage func(content string) string
|
|
|
|
// Commands can actually return either a string, an embed, or a
|
|
// SendMessageData, with error as the second argument.
|
|
|
|
// All registered method contexts:
|
|
Events []*MethodContext
|
|
Commands []*MethodContext
|
|
plumbed *MethodContext
|
|
|
|
// Global middlewares.
|
|
globalmws []*MiddlewareContext
|
|
|
|
// Directly to struct
|
|
cmdValue reflect.Value
|
|
cmdType reflect.Type
|
|
|
|
// Pointer value
|
|
ptrValue reflect.Value
|
|
ptrType reflect.Type
|
|
|
|
helper func() string
|
|
command interface{}
|
|
}
|
|
|
|
// CanSetup is used for subcommands to change variables, such as Description.
|
|
// This method will be triggered when InitCommands is called, which is during
|
|
// New for Context and during RegisterSubcommand for subcommands.
|
|
type CanSetup interface {
|
|
// Setup should panic when it has an error.
|
|
Setup(*Subcommand)
|
|
}
|
|
|
|
// CanHelp is an interface that subcommands can implement to return its own help
|
|
// message. Those messages will automatically be indented into suitable sections
|
|
// by the default Help() implementation. Unlike Usager or CanSetup, the Help()
|
|
// method will be called every time it's needed.
|
|
type CanHelp interface {
|
|
Help() string
|
|
}
|
|
|
|
// NewSubcommand is used to make a new subcommand. You usually wouldn't call
|
|
// this function, but instead use (*Context).RegisterSubcommand().
|
|
func NewSubcommand(cmd interface{}) (*Subcommand, error) {
|
|
var sub = Subcommand{
|
|
command: cmd,
|
|
SanitizeMessage: func(c string) string {
|
|
return c
|
|
},
|
|
}
|
|
|
|
if err := sub.reflectCommands(); err != nil {
|
|
return nil, errors.Wrap(err, "failed to reflect commands")
|
|
}
|
|
|
|
if err := sub.parseCommands(); err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse commands")
|
|
}
|
|
|
|
return &sub, nil
|
|
}
|
|
|
|
// NeedsName sets the name for this subcommand. Like InitCommands, this
|
|
// shouldn't be called at all, rather you should use RegisterSubcommand.
|
|
func (sub *Subcommand) NeedsName() {
|
|
sub.StructName = sub.cmdType.Name()
|
|
sub.Command = lowerFirstLetter(sub.StructName)
|
|
}
|
|
|
|
func lowerFirstLetter(name string) string {
|
|
return strings.ToLower(string(name[0])) + name[1:]
|
|
}
|
|
|
|
// FindCommand finds the MethodContext using either the given method or the
|
|
// given method name. It panics if the given method is not found.
|
|
//
|
|
// There are two ways to use FindCommand:
|
|
//
|
|
// sub.FindCommand("MethodName")
|
|
// sub.FindCommand(thing.MethodName)
|
|
//
|
|
func (sub *Subcommand) FindCommand(method interface{}) *MethodContext {
|
|
return sub.findMethod(method, false)
|
|
}
|
|
|
|
func (sub *Subcommand) findMethod(method interface{}, inclEvents bool) *MethodContext {
|
|
methodName, ok := method.(string)
|
|
if !ok {
|
|
methodName = runtimeMethodName(method)
|
|
}
|
|
|
|
for _, c := range sub.Commands {
|
|
if c.MethodName == methodName {
|
|
return c
|
|
}
|
|
}
|
|
|
|
if inclEvents {
|
|
for _, ev := range sub.Events {
|
|
if ev.MethodName == methodName {
|
|
return ev
|
|
}
|
|
}
|
|
}
|
|
|
|
panic("can't find method " + methodName)
|
|
}
|
|
|
|
// runtimeMethodName returns the name of the method from the given method call.
|
|
// It is used as such:
|
|
//
|
|
// fmt.Println(methodName(t.Method_dash))
|
|
// // Output: main.T.Method_dash-fm
|
|
//
|
|
func runtimeMethodName(v interface{}) string {
|
|
// https://github.com/diamondburned/arikawa/issues/146
|
|
|
|
ptr := reflect.ValueOf(v).Pointer()
|
|
|
|
funcPC := runtime.FuncForPC(ptr)
|
|
if funcPC == nil {
|
|
panic("given method is not a function")
|
|
}
|
|
|
|
funcName := funcPC.Name()
|
|
|
|
// Do weird string parsing because Go wants us to.
|
|
nameParts := strings.Split(funcName, ".")
|
|
mName := nameParts[len(nameParts)-1]
|
|
nameParts = strings.Split(mName, "-")
|
|
if len(nameParts) > 1 { // extract the string before -fm if possible
|
|
mName = nameParts[len(nameParts)-2]
|
|
}
|
|
|
|
return mName
|
|
}
|
|
|
|
// ChangeCommandInfo changes the matched method's Command and Description.
|
|
// Empty means unchanged. This function panics if the given method is not found.
|
|
func (sub *Subcommand) ChangeCommandInfo(method interface{}, cmd, desc string) {
|
|
var command = sub.FindCommand(method)
|
|
if cmd != "" {
|
|
command.Command = cmd
|
|
}
|
|
if desc != "" {
|
|
command.Description = desc
|
|
}
|
|
}
|
|
|
|
// Help calls the subcommand's Help() or auto-generates one with HelpGenerate()
|
|
// if the subcommand doesn't implement CanHelp. It doesn't show hidden commands
|
|
// by default.
|
|
func (sub *Subcommand) Help() string {
|
|
return sub.HelpShowHidden(false)
|
|
}
|
|
|
|
// HelpShowHidden does the same as Help(), except it will render hidden commands
|
|
// if the subcommand doesn't implement CanHelp and showHiddeen is true.
|
|
func (sub *Subcommand) HelpShowHidden(showHidden bool) string {
|
|
// Check if the subcommand implements CanHelp.
|
|
if sub.helper != nil {
|
|
return sub.helper()
|
|
}
|
|
return sub.HelpGenerate(showHidden)
|
|
}
|
|
|
|
// HelpGenerate auto-generates a help message, which contains only a list of
|
|
// commands. It does not print the subcommand header. Use this only if you want
|
|
// to override the Subcommand's help, else use Help(). This function will show
|
|
// hidden commands if showHidden is true.
|
|
func (sub *Subcommand) HelpGenerate(showHidden bool) string {
|
|
// A wider space character.
|
|
const space = "\u2000"
|
|
|
|
var buf strings.Builder
|
|
|
|
for i, cmd := range sub.Commands {
|
|
if cmd.Hidden && !showHidden {
|
|
continue
|
|
}
|
|
|
|
buf.WriteString(sub.Command)
|
|
|
|
if !sub.IsPlumbed() {
|
|
buf.WriteByte(' ')
|
|
buf.WriteString(cmd.Command)
|
|
}
|
|
|
|
for _, alias := range cmd.Aliases {
|
|
buf.WriteByte('|')
|
|
buf.WriteString(alias)
|
|
}
|
|
|
|
// Write the usages first.
|
|
var usages = cmd.Usage()
|
|
|
|
for _, usage := range usages {
|
|
// Uses \u2000, which is wider than a space.
|
|
buf.WriteString(space + "__") // const concat
|
|
buf.WriteString(usage)
|
|
buf.WriteString("__")
|
|
}
|
|
|
|
// Is the last argument trailing? If so, append ellipsis.
|
|
if len(usages) > 0 && cmd.Variadic {
|
|
buf.WriteString("...")
|
|
}
|
|
|
|
// Write the description if there's any.
|
|
if cmd.Description != "" {
|
|
buf.WriteString(": ")
|
|
buf.WriteString(cmd.Description)
|
|
}
|
|
|
|
// Add a new line if this isn't the last command.
|
|
if i != len(sub.Commands)-1 {
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// Hide marks a command as hidden, meaning it won't be shown in help and its
|
|
// UnknownCommand errors will be suppressed.
|
|
func (sub *Subcommand) Hide(method interface{}) {
|
|
sub.FindCommand(method).Hidden = true
|
|
}
|
|
|
|
func (sub *Subcommand) reflectCommands() error {
|
|
t := reflect.TypeOf(sub.command)
|
|
v := reflect.ValueOf(sub.command)
|
|
|
|
if t.Kind() != reflect.Ptr {
|
|
return errors.New("sub is not a pointer")
|
|
}
|
|
|
|
// Set the pointer fields
|
|
sub.ptrValue = v
|
|
sub.ptrType = t
|
|
|
|
ts := t.Elem()
|
|
vs := v.Elem()
|
|
|
|
if ts.Kind() != reflect.Struct {
|
|
return errors.New("sub is not pointer to struct")
|
|
}
|
|
|
|
// Set the struct fields
|
|
sub.cmdValue = vs
|
|
sub.cmdType = ts
|
|
|
|
return nil
|
|
}
|
|
|
|
// InitCommands fills a Subcommand with a context. This shouldn't be called at
|
|
// all, rather you should use the RegisterSubcommand method of a Context.
|
|
func (sub *Subcommand) InitCommands(ctx *Context) error {
|
|
// Start filling up a *Context field
|
|
if err := sub.fillStruct(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// See if struct implements CanSetup:
|
|
if v, ok := sub.command.(CanSetup); ok {
|
|
v.Setup(sub)
|
|
}
|
|
|
|
// See if struct implements CanHelper:
|
|
if v, ok := sub.command.(CanHelp); ok {
|
|
sub.helper = v.Help
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sub *Subcommand) fillStruct(ctx *Context) error {
|
|
for i := 0; i < sub.cmdValue.NumField(); i++ {
|
|
field := sub.cmdValue.Field(i)
|
|
|
|
if !field.CanSet() || !field.CanInterface() {
|
|
continue
|
|
}
|
|
|
|
if _, ok := field.Interface().(*Context); !ok {
|
|
continue
|
|
}
|
|
|
|
field.Set(reflect.ValueOf(ctx))
|
|
return nil
|
|
}
|
|
|
|
return errors.New("no fields with *bot.Context found")
|
|
}
|
|
|
|
func (sub *Subcommand) parseCommands() error {
|
|
var numMethods = sub.ptrValue.NumMethod()
|
|
|
|
for i := 0; i < numMethods; i++ {
|
|
method := sub.ptrValue.Method(i)
|
|
|
|
if !method.CanInterface() {
|
|
continue
|
|
}
|
|
|
|
methodT := sub.ptrType.Method(i)
|
|
if methodT.Name == "Setup" && methodT.Type == typeSetupFn {
|
|
continue
|
|
}
|
|
|
|
cctx := parseMethod(method, methodT)
|
|
if cctx == nil {
|
|
continue
|
|
}
|
|
|
|
// Append.
|
|
if cctx.event == typeMessageCreate {
|
|
sub.Commands = append(sub.Commands, cctx)
|
|
} else {
|
|
sub.Events = append(sub.Events, cctx)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddMiddleware adds a middleware into multiple or all methods, including
|
|
// commands and events. Multiple method names can be comma-delimited. For all
|
|
// methods, use a star (*). The given middleware argument can either be a
|
|
// function with one of the allowed methods or a *MiddlewareContext.
|
|
//
|
|
// Allowed function signatures
|
|
//
|
|
// Below are the acceptable function signatures that would be parsed as a proper
|
|
// middleware. A return value of type T will be ignored. If the given function
|
|
// is invalid, then this method will panic.
|
|
//
|
|
// func(<AnyEvent>) (T, error)
|
|
// func(<AnyEvent>) error
|
|
// func(<AnyEvent>)
|
|
//
|
|
// Note that although technically all of the above function signatures are
|
|
// acceptable, one should almost always return only an error.
|
|
func (sub *Subcommand) AddMiddleware(method, middleware interface{}) {
|
|
var mw *MiddlewareContext
|
|
// Allow *MiddlewareContext to be passed into.
|
|
if v, ok := middleware.(*MiddlewareContext); ok {
|
|
mw = v
|
|
} else {
|
|
mw = ParseMiddleware(middleware)
|
|
}
|
|
|
|
switch v := method.(type) {
|
|
case string:
|
|
sub.addMiddleware(mw, strings.Split(v, ","))
|
|
case []string:
|
|
sub.addMiddleware(mw, v)
|
|
default:
|
|
sub.findMethod(v, true).addMiddleware(mw)
|
|
}
|
|
}
|
|
|
|
func (sub *Subcommand) addMiddleware(mw *MiddlewareContext, methods []string) {
|
|
for _, method := range methods {
|
|
// Trim space.
|
|
if method = strings.TrimSpace(method); method == "*" {
|
|
// Append middleware to global middleware slice.
|
|
sub.globalmws = append(sub.globalmws, mw)
|
|
continue
|
|
}
|
|
// Append middleware to that individual function.
|
|
sub.findMethod(method, true).addMiddleware(mw)
|
|
}
|
|
}
|
|
|
|
func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
|
|
// Search for global middlewares.
|
|
for _, mw := range sub.globalmws {
|
|
if mw.isEvent(evT) {
|
|
callers = append(callers, mw)
|
|
}
|
|
}
|
|
|
|
// Search for specific handlers.
|
|
for _, cctx := range sub.Events {
|
|
// We only take middlewares and callers if the event matches and is not
|
|
// a MessageCreate. The other function already handles that.
|
|
if cctx.isEvent(evT) {
|
|
// Add the command's middlewares first.
|
|
for _, mw := range cctx.middlewares {
|
|
// Concrete struct to interface conversion done implicitly.
|
|
callers = append(callers, mw)
|
|
}
|
|
|
|
callers = append(callers, cctx)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// IsPlumbed returns true if the subcommand is plumbed.
|
|
func (sub *Subcommand) IsPlumbed() bool {
|
|
return sub.plumbed != nil
|
|
}
|
|
|
|
// SetPlumb sets the method as the plumbed command. If method is nil, then the
|
|
// plumbing is also disabled.
|
|
func (sub *Subcommand) SetPlumb(method interface{}) {
|
|
// Ensure that SetPlumb isn't being called on the main context.
|
|
if sub.Command == "" {
|
|
panic("invalid SetPlumb call on *Context")
|
|
}
|
|
|
|
if method == nil {
|
|
sub.plumbed = nil
|
|
return
|
|
}
|
|
|
|
sub.plumbed = sub.FindCommand(method)
|
|
}
|
|
|
|
// AddAliases add alias(es) to specific command (defined with commandName).
|
|
func (sub *Subcommand) AddAliases(commandName interface{}, aliases ...string) {
|
|
// Get command
|
|
command := sub.FindCommand(commandName)
|
|
|
|
// Write new listing of aliases
|
|
command.Aliases = append(command.Aliases, aliases...)
|
|
}
|
|
|
|
// DeriveIntents derives all possible gateway intents from the method handlers
|
|
// and middlewares.
|
|
func (sub *Subcommand) DeriveIntents() gateway.Intents {
|
|
var intents gateway.Intents
|
|
|
|
for _, event := range sub.Events {
|
|
intents |= event.intents()
|
|
}
|
|
for _, command := range sub.Commands {
|
|
intents |= command.intents()
|
|
}
|
|
if sub.IsPlumbed() {
|
|
intents |= sub.plumbed.intents()
|
|
}
|
|
for _, middleware := range sub.globalmws {
|
|
intents |= middleware.intents()
|
|
}
|
|
|
|
return intents
|
|
}
|