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 }