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 }