github.com/diamondburned/arikawa/v2@v2.1.0/bot/subcommand.go (about)

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