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 }