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

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