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

     1  package bot
     2  
     3  import (
     4  	"log"
     5  	"os"
     6  	"os/signal"
     7  	"strings"
     8  	"sync"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/diamondburned/arikawa/api"
    13  	"github.com/diamondburned/arikawa/bot/extras/shellwords"
    14  	"github.com/diamondburned/arikawa/gateway"
    15  	"github.com/diamondburned/arikawa/state"
    16  )
    17  
    18  // Prefixer checks a message if it starts with the desired prefix. By default,
    19  // NewPrefix() is used.
    20  type Prefixer func(*gateway.MessageCreateEvent) (prefix string, ok bool)
    21  
    22  // NewPrefix creates a simple prefix checker using strings. As the default
    23  // prefix is "!", the function is called as NewPrefix("!").
    24  func NewPrefix(prefixes ...string) Prefixer {
    25  	return func(msg *gateway.MessageCreateEvent) (string, bool) {
    26  		for _, prefix := range prefixes {
    27  			if strings.HasPrefix(msg.Content, prefix) {
    28  				return prefix, true
    29  			}
    30  		}
    31  		return "", false
    32  	}
    33  }
    34  
    35  // ArgsParser is the function type for parsing message content into fields,
    36  // usually delimited by spaces.
    37  type ArgsParser func(content string) ([]string, error)
    38  
    39  // DefaultArgsParser implements a parser similar to that of shell's,
    40  // implementing quotes as well as escapes.
    41  func DefaultArgsParser() ArgsParser {
    42  	return shellwords.Parse
    43  }
    44  
    45  // Context is the bot state for commands and subcommands.
    46  //
    47  // Commands
    48  //
    49  // A command can be created by making it a method of Commands, or whatever
    50  // struct was given to the constructor. This following example creates a command
    51  // with a single integer argument (which can be ran with "~example 123"):
    52  //
    53  //    func (c *Commands) Example(
    54  //        m *gateway.MessageCreateEvent, i int) (string, error) {
    55  //
    56  //        return fmt.Sprintf("You sent: %d", i)
    57  //    }
    58  //
    59  // Commands' exported methods will all be used as commands. Messages are parsed
    60  // with its first argument (the command) mapped accordingly to c.MapName, which
    61  // capitalizes the first letter automatically to reflect the exported method
    62  // name.
    63  //
    64  // A command can either return either an error, or data and error. The only data
    65  // types allowed are string, *discord.Embed, and *api.SendMessageData. Any other
    66  // return types will invalidate the method.
    67  //
    68  // Events
    69  //
    70  // An event can only have one argument, which is the pointer to the event
    71  // struct. It can also only return error.
    72  //
    73  //    func (c *Commands) Example(o *gateway.TypingStartEvent) error {
    74  //        log.Println("Someone's typing!")
    75  //        return nil
    76  //    }
    77  type Context struct {
    78  	*Subcommand
    79  	*state.State
    80  
    81  	// Descriptive (but optional) bot name
    82  	Name string
    83  
    84  	// Descriptive help body
    85  	Description string
    86  
    87  	// Called to parse message content, default to DefaultArgsParser().
    88  	ParseArgs ArgsParser
    89  
    90  	// Called to check a message's prefix. The default prefix is "!". Refer to
    91  	// NewPrefix().
    92  	HasPrefix Prefixer
    93  
    94  	// AllowBot makes the router also process MessageCreate events from bots.
    95  	// This is false by default and only applies to MessageCreate.
    96  	AllowBot bool
    97  
    98  	// QuietUnknownCommand, if true, will not make the bot reply with an unknown
    99  	// command error into the chat. This will apply to all other subcommands.
   100  	// SilentUnknown controls whether or not an ErrUnknownCommand should be
   101  	// returned (instead of a silent error).
   102  	SilentUnknown struct {
   103  		// Command when true will silent only unknown commands. Known
   104  		// subcommands with unknown commands will still error out.
   105  		Command bool
   106  		// Subcommand when true will suppress unknown subcommands.
   107  		Subcommand bool
   108  	}
   109  
   110  	// FormatError formats any errors returned by anything, including the method
   111  	// commands or the reflect functions. This also includes invalid usage
   112  	// errors or unknown command errors. Returning an empty string means
   113  	// ignoring the error.
   114  	//
   115  	// By default, this field replaces all @ with @\u200b, which prevents an
   116  	// @everyone mention.
   117  	FormatError func(error) string
   118  
   119  	// ErrorLogger logs any error that anything makes and the library can't
   120  	// reply to the client. This includes any event callback errors that aren't
   121  	// Message Create.
   122  	ErrorLogger func(error)
   123  
   124  	// ReplyError when true replies to the user the error. This only applies to
   125  	// MessageCreate events.
   126  	ReplyError bool
   127  
   128  	// EditableCommands when true will also listen for MessageUpdateEvent and
   129  	// treat them as newly created messages. This is convenient if you want
   130  	// to quickly edit a message and re-execute the command.
   131  	EditableCommands bool
   132  
   133  	// Subcommands contains all the registered subcommands. This is not
   134  	// exported, as it shouldn't be used directly.
   135  	subcommands []*Subcommand
   136  
   137  	// Quick access map from event types to pointers. This map will never have
   138  	// MessageCreateEvent's type.
   139  	typeCache sync.Map // map[reflect.Type][]*CommandContext
   140  }
   141  
   142  // Start quickly starts a bot with the given command. It will prepend "Bot"
   143  // into the token automatically. Refer to example/ for usage.
   144  func Start(
   145  	token string, cmd interface{},
   146  	opts func(*Context) error) (wait func() error, err error) {
   147  
   148  	s, err := state.New("Bot " + token)
   149  	if err != nil {
   150  		return nil, errors.Wrap(err, "failed to create a dgo session")
   151  	}
   152  
   153  	c, err := New(s, cmd)
   154  	if err != nil {
   155  		return nil, errors.Wrap(err, "failed to create rfrouter")
   156  	}
   157  
   158  	s.Gateway.ErrorLog = func(err error) {
   159  		c.ErrorLogger(err)
   160  	}
   161  
   162  	if opts != nil {
   163  		if err := opts(c); err != nil {
   164  			return nil, err
   165  		}
   166  	}
   167  
   168  	cancel := c.Start()
   169  
   170  	if err := s.Open(); err != nil {
   171  		return nil, errors.Wrap(err, "failed to connect to Discord")
   172  	}
   173  
   174  	return func() error {
   175  		Wait()
   176  		// remove handler first
   177  		cancel()
   178  		// then finish closing session
   179  		return s.Close()
   180  	}, nil
   181  }
   182  
   183  // Wait blocks until SIGINT.
   184  func Wait() {
   185  	sigs := make(chan os.Signal, 1)
   186  	signal.Notify(sigs, os.Interrupt)
   187  	<-sigs
   188  }
   189  
   190  // New makes a new context with a "~" as the prefix. cmds must be a pointer to a
   191  // struct with a *Context field. Example:
   192  //
   193  //    type Commands struct {
   194  //        Ctx *Context
   195  //    }
   196  //
   197  //    cmds := &Commands{}
   198  //    c, err := bot.New(session, cmds)
   199  //
   200  // The default prefix is "~", which means commands must start with "~" followed
   201  // by the command name in the first argument, else it will be ignored.
   202  //
   203  // c.Start() should be called afterwards to actually handle incoming events.
   204  func New(s *state.State, cmd interface{}) (*Context, error) {
   205  	c, err := NewSubcommand(cmd)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	ctx := &Context{
   211  		Subcommand: c,
   212  		State:      s,
   213  		ParseArgs:  DefaultArgsParser(),
   214  		HasPrefix:  NewPrefix("~"),
   215  		FormatError: func(err error) string {
   216  			// Escape all pings, including @everyone.
   217  			return strings.Replace(err.Error(), "@", "@\u200b", -1)
   218  		},
   219  		ErrorLogger: func(err error) {
   220  			log.Println("Bot error:", err)
   221  		},
   222  		ReplyError: true,
   223  	}
   224  
   225  	if err := ctx.InitCommands(ctx); err != nil {
   226  		return nil, errors.Wrap(err, "failed to initialize with given cmds")
   227  	}
   228  
   229  	return ctx, nil
   230  }
   231  
   232  // AddIntent adds the given Gateway Intent into the Gateway. This is a
   233  // convenient function that calls Gateway's AddIntent.
   234  func (ctx *Context) AddIntent(i gateway.Intents) {
   235  	ctx.Gateway.AddIntent(i)
   236  }
   237  
   238  // Subcommands returns the slice of subcommands. To add subcommands, use
   239  // RegisterSubcommand().
   240  func (ctx *Context) Subcommands() []*Subcommand {
   241  	// Getter is not useless, refer to the struct doc for reason.
   242  	return ctx.subcommands
   243  }
   244  
   245  // FindMethod finds a method based on the struct and method name. The queried
   246  // names will have their flags stripped.
   247  //
   248  //    // Find a command from the main context:
   249  //    cmd := ctx.FindMethod("", "Method")
   250  //    // Find a command from a subcommand:
   251  //    cmd  = ctx.FindMethod("Starboard", "Reset")
   252  //
   253  func (ctx *Context) FindCommand(structName, methodName string) *MethodContext {
   254  	if structName == "" {
   255  		return ctx.Subcommand.FindCommand(methodName)
   256  	}
   257  	for _, sub := range ctx.subcommands {
   258  		if sub.StructName == structName {
   259  			return sub.FindCommand(methodName)
   260  		}
   261  	}
   262  	return nil
   263  }
   264  
   265  // MustRegisterSubcommand tries to register a subcommand, and will panic if it
   266  // fails. This is recommended, as subcommands won't change after initializing
   267  // once in runtime, thus fairly harmless after development.
   268  func (ctx *Context) MustRegisterSubcommand(cmd interface{}) *Subcommand {
   269  	return ctx.MustRegisterSubcommandCustom(cmd, "")
   270  }
   271  
   272  // MustRegisterSubcommandCustom works similarly to MustRegisterSubcommand, but
   273  // takes an extra argument for a command name override.
   274  func (ctx *Context) MustRegisterSubcommandCustom(cmd interface{}, name string) *Subcommand {
   275  	s, err := ctx.RegisterSubcommandCustom(cmd, name)
   276  	if err != nil {
   277  		panic(err)
   278  	}
   279  	return s
   280  }
   281  
   282  // RegisterSubcommand registers and adds cmd to the list of subcommands. It will
   283  // also return the resulting Subcommand.
   284  func (ctx *Context) RegisterSubcommand(cmd interface{}) (*Subcommand, error) {
   285  	return ctx.RegisterSubcommandCustom(cmd, "")
   286  }
   287  
   288  // RegisterSubcommand registers and adds cmd to the list of subcommands with a
   289  // custom command name (optional).
   290  func (ctx *Context) RegisterSubcommandCustom(cmd interface{}, name string) (*Subcommand, error) {
   291  	s, err := NewSubcommand(cmd)
   292  	if err != nil {
   293  		return nil, errors.Wrap(err, "failed to add subcommand")
   294  	}
   295  
   296  	// Register the subcommand's name.
   297  	s.NeedsName()
   298  
   299  	if name != "" {
   300  		s.Command = name
   301  	}
   302  
   303  	if err := s.InitCommands(ctx); err != nil {
   304  		return nil, errors.Wrap(err, "failed to initialize subcommand")
   305  	}
   306  
   307  	// Do a collision check
   308  	for _, sub := range ctx.subcommands {
   309  		if sub.Command == s.Command {
   310  			return nil, errors.New("new subcommand has duplicate name: " + s.Command)
   311  		}
   312  	}
   313  
   314  	ctx.subcommands = append(ctx.subcommands, s)
   315  	return s, nil
   316  }
   317  
   318  // Start adds itself into the session handlers. This needs to be run. The
   319  // returned function is a delete function, which removes itself from the
   320  // Session handlers.
   321  func (ctx *Context) Start() func() {
   322  	return ctx.State.AddHandler(func(v interface{}) {
   323  		err := ctx.callCmd(v)
   324  		if err == nil {
   325  			return
   326  		}
   327  
   328  		str := ctx.FormatError(err)
   329  		if str == "" {
   330  			return
   331  		}
   332  
   333  		mc, isMessage := v.(*gateway.MessageCreateEvent)
   334  
   335  		// Log the main error if reply is disabled or if the event isn't a
   336  		// message.
   337  		if !ctx.ReplyError || !isMessage {
   338  			// Ignore trivial errors:
   339  			switch err.(type) {
   340  			case *ErrInvalidUsage, *ErrUnknownCommand:
   341  				// Ignore
   342  			default:
   343  				ctx.ErrorLogger(errors.Wrap(err, "command error"))
   344  			}
   345  
   346  			return
   347  		}
   348  
   349  		// Only reply if the event is not a message.
   350  		if !isMessage {
   351  			return
   352  		}
   353  
   354  		_, err = ctx.SendMessageComplex(mc.ChannelID, api.SendMessageData{
   355  			// Escape the error using the message sanitizer:
   356  			Content: ctx.SanitizeMessage(str),
   357  			AllowedMentions: &api.AllowedMentions{
   358  				// Don't allow mentions.
   359  				Parse: []api.AllowedMentionType{},
   360  			},
   361  		})
   362  		if err != nil {
   363  			ctx.ErrorLogger(err)
   364  
   365  			// TODO: there ought to be a better way lol
   366  		}
   367  	})
   368  }
   369  
   370  // Call should only be used if you know what you're doing.
   371  func (ctx *Context) Call(event interface{}) error {
   372  	return ctx.callCmd(event)
   373  }
   374  
   375  // Help generates a full Help message. It serves mainly as a reference for
   376  // people to reimplement and change. It doesn't show hidden commands.
   377  func (ctx *Context) Help() string {
   378  	return ctx.HelpGenerate(false)
   379  }
   380  
   381  // HelpGenerate generates a full Help message. It serves mainly as a reference
   382  // for people to reimplement and change. If showHidden is true, then hidden
   383  // subcommands and commands will be shown.
   384  func (ctx *Context) HelpGenerate(showHidden bool) string {
   385  	// Generate the header.
   386  	buf := strings.Builder{}
   387  	buf.WriteString("__Help__")
   388  
   389  	// Name an
   390  	if ctx.Name != "" {
   391  		buf.WriteString(": " + ctx.Name)
   392  	}
   393  	if ctx.Description != "" {
   394  		buf.WriteString("\n" + IndentLines(ctx.Description))
   395  	}
   396  
   397  	// Separators
   398  	buf.WriteString("\n---\n")
   399  
   400  	// Generate all commands
   401  	if help := ctx.Subcommand.Help(); help != "" {
   402  		buf.WriteString("__Commands__\n")
   403  		buf.WriteString(IndentLines(help))
   404  		buf.WriteByte('\n')
   405  	}
   406  
   407  	var subcommands = ctx.Subcommands()
   408  	var subhelps = make([]string, 0, len(subcommands))
   409  
   410  	for _, sub := range subcommands {
   411  		if sub.Hidden && !showHidden {
   412  			continue
   413  		}
   414  
   415  		help := sub.HelpShowHidden(showHidden)
   416  		if help == "" {
   417  			continue
   418  		}
   419  		help = IndentLines(help)
   420  
   421  		var header = "**" + sub.Command + "**"
   422  		if sub.Description != "" {
   423  			header += ": " + sub.Description
   424  		}
   425  
   426  		subhelps = append(subhelps, header+"\n"+help)
   427  	}
   428  
   429  	if len(subhelps) > 0 {
   430  		buf.WriteString("---\n")
   431  		buf.WriteString("__Subcommands__\n")
   432  		buf.WriteString(IndentLines(strings.Join(subhelps, "\n")))
   433  	}
   434  
   435  	return buf.String()
   436  }
   437  
   438  // IndentLine prefixes every line from input with a single-level indentation.
   439  func IndentLines(input string) string {
   440  	const indent = "      "
   441  	var lines = strings.Split(input, "\n")
   442  	for i := range lines {
   443  		lines[i] = indent + lines[i]
   444  	}
   445  	return strings.Join(lines, "\n")
   446  }