github.com/diamondburned/arikawa@v1.3.14/bot/subcommand.go (about)

     1  package bot
     2  
     3  import (
     4  	"reflect"
     5  	"strings"
     6  
     7  	"github.com/pkg/errors"
     8  
     9  	"github.com/diamondburned/arikawa/gateway"
    10  )
    11  
    12  var (
    13  	typeMessageCreate = reflect.TypeOf((*gateway.MessageCreateEvent)(nil))
    14  	typeMessageUpdate = reflect.TypeOf((*gateway.MessageUpdateEvent)(nil))
    15  
    16  	typeIError  = reflect.TypeOf((*error)(nil)).Elem()
    17  	typeIManP   = reflect.TypeOf((*ManualParser)(nil)).Elem()
    18  	typeICusP   = reflect.TypeOf((*CustomParser)(nil)).Elem()
    19  	typeIParser = reflect.TypeOf((*Parser)(nil)).Elem()
    20  	typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem()
    21  	typeSetupFn = methodType((*CanSetup)(nil), "Setup")
    22  )
    23  
    24  func methodType(iface interface{}, name string) reflect.Type {
    25  	method, _ := reflect.TypeOf(iface).
    26  		Elem().
    27  		MethodByName(name)
    28  	return method.Type
    29  }
    30  
    31  // HelpUnderline formats command arguments with an underline, similar to
    32  // manpages.
    33  var HelpUnderline = true
    34  
    35  func underline(word string) string {
    36  	if HelpUnderline {
    37  		return "__" + word + "__"
    38  	}
    39  	return word
    40  }
    41  
    42  // Subcommand is any form of command, which could be a top-level command or a
    43  // subcommand.
    44  //
    45  // Allowed method signatures
    46  //
    47  // These are the acceptable function signatures that would be parsed as commands
    48  // or events. A return type <T> implies that return value will be ignored.
    49  //
    50  //    func(*gateway.MessageCreateEvent, ...) (string, error)
    51  //    func(*gateway.MessageCreateEvent, ...) (*discord.Embed, error)
    52  //    func(*gateway.MessageCreateEvent, ...) (*api.SendMessageData, error)
    53  //    func(*gateway.MessageCreateEvent, ...) (T, error)
    54  //    func(*gateway.MessageCreateEvent, ...) error
    55  //    func(*gateway.MessageCreateEvent, ...)
    56  //    func(<AnyEvent>) (T, error)
    57  //    func(<AnyEvent>) error
    58  //    func(<AnyEvent>)
    59  //
    60  type Subcommand struct {
    61  	// Description is a string that's appended after the subcommand name in
    62  	// (*Context).Help().
    63  	Description string
    64  
    65  	// Hidden if true will not be shown by (*Context).Help(). It will
    66  	// also cause unknown command errors to be suppressed.
    67  	Hidden bool
    68  
    69  	// Raw struct name, including the flag (only filled for actual subcommands,
    70  	// will be empty for Context):
    71  	StructName string
    72  	// Parsed command name:
    73  	Command string
    74  
    75  	// SanitizeMessage is executed on the message content if the method returns
    76  	// a string content or a SendMessageData.
    77  	SanitizeMessage func(content string) string
    78  
    79  	// Commands can actually return either a string, an embed, or a
    80  	// SendMessageData, with error as the second argument.
    81  
    82  	// All registered method contexts:
    83  	Events   []*MethodContext
    84  	Commands []*MethodContext
    85  	plumbed  *MethodContext
    86  
    87  	// Global middlewares.
    88  	globalmws []*MiddlewareContext
    89  
    90  	// Directly to struct
    91  	cmdValue reflect.Value
    92  	cmdType  reflect.Type
    93  
    94  	// Pointer value
    95  	ptrValue reflect.Value
    96  	ptrType  reflect.Type
    97  
    98  	helper  func() string
    99  	command interface{}
   100  }
   101  
   102  // CanSetup is used for subcommands to change variables, such as Description.
   103  // This method will be triggered when InitCommands is called, which is during
   104  // New for Context and during RegisterSubcommand for subcommands.
   105  type CanSetup interface {
   106  	// Setup should panic when it has an error.
   107  	Setup(*Subcommand)
   108  }
   109  
   110  // CanHelp is an interface that subcommands can implement to return its own help
   111  // message. Those messages will automatically be indented into suitable sections
   112  // by the default Help() implementation. Unlike Usager or CanSetup, the Help()
   113  // method will be called every time it's needed.
   114  type CanHelp interface {
   115  	Help() string
   116  }
   117  
   118  // NewSubcommand is used to make a new subcommand. You usually wouldn't call
   119  // this function, but instead use (*Context).RegisterSubcommand().
   120  func NewSubcommand(cmd interface{}) (*Subcommand, error) {
   121  	var sub = Subcommand{
   122  		command: cmd,
   123  		SanitizeMessage: func(c string) string {
   124  			return c
   125  		},
   126  	}
   127  
   128  	if err := sub.reflectCommands(); err != nil {
   129  		return nil, errors.Wrap(err, "failed to reflect commands")
   130  	}
   131  
   132  	if err := sub.parseCommands(); err != nil {
   133  		return nil, errors.Wrap(err, "failed to parse commands")
   134  	}
   135  
   136  	return &sub, nil
   137  }
   138  
   139  // NeedsName sets the name for this subcommand. Like InitCommands, this
   140  // shouldn't be called at all, rather you should use RegisterSubcommand.
   141  func (sub *Subcommand) NeedsName() {
   142  	sub.StructName = sub.cmdType.Name()
   143  	sub.Command = lowerFirstLetter(sub.StructName)
   144  }
   145  
   146  // FindCommand finds the MethodContext. It panics if methodName is not found.
   147  func (sub *Subcommand) FindCommand(methodName string) *MethodContext {
   148  	for _, c := range sub.Commands {
   149  		if c.MethodName == methodName {
   150  			return c
   151  		}
   152  	}
   153  	panic("Can't find method " + methodName)
   154  }
   155  
   156  // ChangeCommandInfo changes the matched methodName's Command and Description.
   157  // Empty means unchanged. This function panics if methodName is not found.
   158  func (sub *Subcommand) ChangeCommandInfo(methodName, cmd, desc string) {
   159  	var command = sub.FindCommand(methodName)
   160  	if cmd != "" {
   161  		command.Command = cmd
   162  	}
   163  	if desc != "" {
   164  		command.Description = desc
   165  	}
   166  }
   167  
   168  // Help calls the subcommand's Help() or auto-generates one with HelpGenerate()
   169  // if the subcommand doesn't implement CanHelp. It doesn't show hidden commands
   170  // by default.
   171  func (sub *Subcommand) Help() string {
   172  	return sub.HelpShowHidden(false)
   173  }
   174  
   175  // HelpShowHidden does the same as Help(), except it will render hidden commands
   176  // if the subcommand doesn't implement CanHelp and showHiddeen is true.
   177  func (sub *Subcommand) HelpShowHidden(showHidden bool) string {
   178  	// Check if the subcommand implements CanHelp.
   179  	if sub.helper != nil {
   180  		return sub.helper()
   181  	}
   182  	return sub.HelpGenerate(showHidden)
   183  }
   184  
   185  // HelpGenerate auto-generates a help message. Use this only if you want to
   186  // override the Subcommand's help, else use Help(). This function will show
   187  // hidden commands if showHidden is true.
   188  func (sub *Subcommand) HelpGenerate(showHidden bool) string {
   189  	// A wider space character.
   190  	const s = "\u2000"
   191  
   192  	var buf strings.Builder
   193  
   194  	for i, cmd := range sub.Commands {
   195  		if cmd.Hidden && !showHidden {
   196  			continue
   197  		}
   198  
   199  		buf.WriteString(sub.Command + " " + cmd.Command)
   200  
   201  		// Write the usages first.
   202  		for _, usage := range cmd.Usage() {
   203  			// Is the last argument trailing? If so, append ellipsis.
   204  			if cmd.Variadic {
   205  				usage += "..."
   206  			}
   207  
   208  			// Uses \u2000, which is wider than a space.
   209  			buf.WriteString(s + "__" + usage + "__")
   210  		}
   211  
   212  		// Write the description if there's any.
   213  		if cmd.Description != "" {
   214  			buf.WriteString(": " + cmd.Description)
   215  		}
   216  
   217  		// Add a new line if this isn't the last command.
   218  		if i != len(sub.Commands)-1 {
   219  			buf.WriteByte('\n')
   220  		}
   221  	}
   222  
   223  	return buf.String()
   224  }
   225  
   226  // Hide marks a command as hidden, meaning it won't be shown in help and its
   227  // UnknownCommand errors will be suppressed.
   228  func (sub *Subcommand) Hide(methodName string) {
   229  	sub.FindCommand(methodName).Hidden = true
   230  }
   231  
   232  func (sub *Subcommand) reflectCommands() error {
   233  	t := reflect.TypeOf(sub.command)
   234  	v := reflect.ValueOf(sub.command)
   235  
   236  	if t.Kind() != reflect.Ptr {
   237  		return errors.New("sub is not a pointer")
   238  	}
   239  
   240  	// Set the pointer fields
   241  	sub.ptrValue = v
   242  	sub.ptrType = t
   243  
   244  	ts := t.Elem()
   245  	vs := v.Elem()
   246  
   247  	if ts.Kind() != reflect.Struct {
   248  		return errors.New("sub is not pointer to struct")
   249  	}
   250  
   251  	// Set the struct fields
   252  	sub.cmdValue = vs
   253  	sub.cmdType = ts
   254  
   255  	return nil
   256  }
   257  
   258  // InitCommands fills a Subcommand with a context. This shouldn't be called at
   259  // all, rather you should use the RegisterSubcommand method of a Context.
   260  func (sub *Subcommand) InitCommands(ctx *Context) error {
   261  	// Start filling up a *Context field
   262  	if err := sub.fillStruct(ctx); err != nil {
   263  		return err
   264  	}
   265  
   266  	// See if struct implements CanSetup:
   267  	if v, ok := sub.command.(CanSetup); ok {
   268  		v.Setup(sub)
   269  	}
   270  
   271  	// See if struct implements CanHelper:
   272  	if v, ok := sub.command.(CanHelp); ok {
   273  		sub.helper = v.Help
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  func (sub *Subcommand) fillStruct(ctx *Context) error {
   280  	for i := 0; i < sub.cmdValue.NumField(); i++ {
   281  		field := sub.cmdValue.Field(i)
   282  
   283  		if !field.CanSet() || !field.CanInterface() {
   284  			continue
   285  		}
   286  
   287  		if _, ok := field.Interface().(*Context); !ok {
   288  			continue
   289  		}
   290  
   291  		field.Set(reflect.ValueOf(ctx))
   292  		return nil
   293  	}
   294  
   295  	return errors.New("no fields with *bot.Context found")
   296  }
   297  
   298  func (sub *Subcommand) parseCommands() error {
   299  	var numMethods = sub.ptrValue.NumMethod()
   300  
   301  	for i := 0; i < numMethods; i++ {
   302  		method := sub.ptrValue.Method(i)
   303  
   304  		if !method.CanInterface() {
   305  			continue
   306  		}
   307  
   308  		methodT := sub.ptrType.Method(i)
   309  		if methodT.Name == "Setup" && methodT.Type == typeSetupFn {
   310  			continue
   311  		}
   312  
   313  		cctx := parseMethod(method, methodT)
   314  		if cctx == nil {
   315  			continue
   316  		}
   317  
   318  		// Append.
   319  		if cctx.event == typeMessageCreate {
   320  			sub.Commands = append(sub.Commands, cctx)
   321  		} else {
   322  			sub.Events = append(sub.Events, cctx)
   323  		}
   324  	}
   325  
   326  	return nil
   327  }
   328  
   329  // AddMiddleware adds a middleware into multiple or all methods, including
   330  // commands and events. Multiple method names can be comma-delimited. For all
   331  // methods, use a star (*). The given middleware argument can either be a
   332  // function with one of the allowed methods or a *MiddlewareContext.
   333  //
   334  // Allowed function signatures
   335  //
   336  // Below are the acceptable function signatures that would be parsed as a proper
   337  // middleware. A return value of type T will be ignored. If the given function
   338  // is invalid, then this method will panic.
   339  //
   340  //    func(<AnyEvent>) (T, error)
   341  //    func(<AnyEvent>) error
   342  //    func(<AnyEvent>)
   343  //
   344  // Note that although technically all of the above function signatures are
   345  // acceptable, one should almost always return only an error.
   346  func (sub *Subcommand) AddMiddleware(methodName string, middleware interface{}) {
   347  	var mw *MiddlewareContext
   348  	// Allow *MiddlewareContext to be passed into.
   349  	if v, ok := middleware.(*MiddlewareContext); ok {
   350  		mw = v
   351  	} else {
   352  		mw = ParseMiddleware(middleware)
   353  	}
   354  
   355  	// Parse method name:
   356  	for _, method := range strings.Split(methodName, ",") {
   357  		// Trim space.
   358  		if method = strings.TrimSpace(method); method == "*" {
   359  			// Append middleware to global middleware slice.
   360  			sub.globalmws = append(sub.globalmws, mw)
   361  			continue
   362  		}
   363  		// Append middleware to that individual function.
   364  		sub.findMethod(method).addMiddleware(mw)
   365  	}
   366  }
   367  
   368  func (sub *Subcommand) findMethod(name string) *MethodContext {
   369  	for _, ev := range sub.Events {
   370  		if ev.MethodName == name {
   371  			return ev
   372  		}
   373  	}
   374  	return sub.FindCommand(name)
   375  }
   376  
   377  func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
   378  	// Search for global middlewares.
   379  	for _, mw := range sub.globalmws {
   380  		if mw.isEvent(evT) {
   381  			callers = append(callers, mw)
   382  		}
   383  	}
   384  
   385  	// Search for specific handlers.
   386  	for _, cctx := range sub.Events {
   387  		// We only take middlewares and callers if the event matches and is not
   388  		// a MessageCreate. The other function already handles that.
   389  		if cctx.isEvent(evT) {
   390  			// Add the command's middlewares first.
   391  			for _, mw := range cctx.middlewares {
   392  				// Concrete struct to interface conversion done implicitly.
   393  				callers = append(callers, mw)
   394  			}
   395  
   396  			callers = append(callers, cctx)
   397  		}
   398  	}
   399  	return
   400  }
   401  
   402  // SetPlumb sets the method as the plumbed command.
   403  func (sub *Subcommand) SetPlumb(methodName string) {
   404  	sub.plumbed = sub.FindCommand(methodName)
   405  }
   406  
   407  // AddAliases add alias(es) to specific command (defined with commandName).
   408  func (sub *Subcommand) AddAliases(commandName string, aliases ...string) {
   409  	// Get command
   410  	command := sub.FindCommand(commandName)
   411  
   412  	// Write new listing of aliases
   413  	command.Aliases = append(command.Aliases, aliases...)
   414  }
   415  
   416  func lowerFirstLetter(name string) string {
   417  	return strings.ToLower(string(name[0])) + name[1:]
   418  }