github.com/diamondburned/arikawa@v1.3.14/bot/subcommand.go (about) 1 package bot 2 3 import ( 4 "reflect" 5 "strings" 6 7 "github.com/pkg/errors" 8 9 "github.com/diamondburned/arikawa/gateway" 10 ) 11 12 var ( 13 typeMessageCreate = reflect.TypeOf((*gateway.MessageCreateEvent)(nil)) 14 typeMessageUpdate = reflect.TypeOf((*gateway.MessageUpdateEvent)(nil)) 15 16 typeIError = reflect.TypeOf((*error)(nil)).Elem() 17 typeIManP = reflect.TypeOf((*ManualParser)(nil)).Elem() 18 typeICusP = reflect.TypeOf((*CustomParser)(nil)).Elem() 19 typeIParser = reflect.TypeOf((*Parser)(nil)).Elem() 20 typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem() 21 typeSetupFn = methodType((*CanSetup)(nil), "Setup") 22 ) 23 24 func methodType(iface interface{}, name string) reflect.Type { 25 method, _ := reflect.TypeOf(iface). 26 Elem(). 27 MethodByName(name) 28 return method.Type 29 } 30 31 // HelpUnderline formats command arguments with an underline, similar to 32 // manpages. 33 var HelpUnderline = true 34 35 func underline(word string) string { 36 if HelpUnderline { 37 return "__" + word + "__" 38 } 39 return word 40 } 41 42 // Subcommand is any form of command, which could be a top-level command or a 43 // subcommand. 44 // 45 // Allowed method signatures 46 // 47 // These are the acceptable function signatures that would be parsed as commands 48 // or events. A return type <T> implies that return value will be ignored. 49 // 50 // func(*gateway.MessageCreateEvent, ...) (string, error) 51 // func(*gateway.MessageCreateEvent, ...) (*discord.Embed, error) 52 // func(*gateway.MessageCreateEvent, ...) (*api.SendMessageData, error) 53 // func(*gateway.MessageCreateEvent, ...) (T, error) 54 // func(*gateway.MessageCreateEvent, ...) error 55 // func(*gateway.MessageCreateEvent, ...) 56 // func(<AnyEvent>) (T, error) 57 // func(<AnyEvent>) error 58 // func(<AnyEvent>) 59 // 60 type Subcommand struct { 61 // Description is a string that's appended after the subcommand name in 62 // (*Context).Help(). 63 Description string 64 65 // Hidden if true will not be shown by (*Context).Help(). It will 66 // also cause unknown command errors to be suppressed. 67 Hidden bool 68 69 // Raw struct name, including the flag (only filled for actual subcommands, 70 // will be empty for Context): 71 StructName string 72 // Parsed command name: 73 Command string 74 75 // SanitizeMessage is executed on the message content if the method returns 76 // a string content or a SendMessageData. 77 SanitizeMessage func(content string) string 78 79 // Commands can actually return either a string, an embed, or a 80 // SendMessageData, with error as the second argument. 81 82 // All registered method contexts: 83 Events []*MethodContext 84 Commands []*MethodContext 85 plumbed *MethodContext 86 87 // Global middlewares. 88 globalmws []*MiddlewareContext 89 90 // Directly to struct 91 cmdValue reflect.Value 92 cmdType reflect.Type 93 94 // Pointer value 95 ptrValue reflect.Value 96 ptrType reflect.Type 97 98 helper func() string 99 command interface{} 100 } 101 102 // CanSetup is used for subcommands to change variables, such as Description. 103 // This method will be triggered when InitCommands is called, which is during 104 // New for Context and during RegisterSubcommand for subcommands. 105 type CanSetup interface { 106 // Setup should panic when it has an error. 107 Setup(*Subcommand) 108 } 109 110 // CanHelp is an interface that subcommands can implement to return its own help 111 // message. Those messages will automatically be indented into suitable sections 112 // by the default Help() implementation. Unlike Usager or CanSetup, the Help() 113 // method will be called every time it's needed. 114 type CanHelp interface { 115 Help() string 116 } 117 118 // NewSubcommand is used to make a new subcommand. You usually wouldn't call 119 // this function, but instead use (*Context).RegisterSubcommand(). 120 func NewSubcommand(cmd interface{}) (*Subcommand, error) { 121 var sub = Subcommand{ 122 command: cmd, 123 SanitizeMessage: func(c string) string { 124 return c 125 }, 126 } 127 128 if err := sub.reflectCommands(); err != nil { 129 return nil, errors.Wrap(err, "failed to reflect commands") 130 } 131 132 if err := sub.parseCommands(); err != nil { 133 return nil, errors.Wrap(err, "failed to parse commands") 134 } 135 136 return &sub, nil 137 } 138 139 // NeedsName sets the name for this subcommand. Like InitCommands, this 140 // shouldn't be called at all, rather you should use RegisterSubcommand. 141 func (sub *Subcommand) NeedsName() { 142 sub.StructName = sub.cmdType.Name() 143 sub.Command = lowerFirstLetter(sub.StructName) 144 } 145 146 // FindCommand finds the MethodContext. It panics if methodName is not found. 147 func (sub *Subcommand) FindCommand(methodName string) *MethodContext { 148 for _, c := range sub.Commands { 149 if c.MethodName == methodName { 150 return c 151 } 152 } 153 panic("Can't find method " + methodName) 154 } 155 156 // ChangeCommandInfo changes the matched methodName's Command and Description. 157 // Empty means unchanged. This function panics if methodName is not found. 158 func (sub *Subcommand) ChangeCommandInfo(methodName, cmd, desc string) { 159 var command = sub.FindCommand(methodName) 160 if cmd != "" { 161 command.Command = cmd 162 } 163 if desc != "" { 164 command.Description = desc 165 } 166 } 167 168 // Help calls the subcommand's Help() or auto-generates one with HelpGenerate() 169 // if the subcommand doesn't implement CanHelp. It doesn't show hidden commands 170 // by default. 171 func (sub *Subcommand) Help() string { 172 return sub.HelpShowHidden(false) 173 } 174 175 // HelpShowHidden does the same as Help(), except it will render hidden commands 176 // if the subcommand doesn't implement CanHelp and showHiddeen is true. 177 func (sub *Subcommand) HelpShowHidden(showHidden bool) string { 178 // Check if the subcommand implements CanHelp. 179 if sub.helper != nil { 180 return sub.helper() 181 } 182 return sub.HelpGenerate(showHidden) 183 } 184 185 // HelpGenerate auto-generates a help message. Use this only if you want to 186 // override the Subcommand's help, else use Help(). This function will show 187 // hidden commands if showHidden is true. 188 func (sub *Subcommand) HelpGenerate(showHidden bool) string { 189 // A wider space character. 190 const s = "\u2000" 191 192 var buf strings.Builder 193 194 for i, cmd := range sub.Commands { 195 if cmd.Hidden && !showHidden { 196 continue 197 } 198 199 buf.WriteString(sub.Command + " " + cmd.Command) 200 201 // Write the usages first. 202 for _, usage := range cmd.Usage() { 203 // Is the last argument trailing? If so, append ellipsis. 204 if cmd.Variadic { 205 usage += "..." 206 } 207 208 // Uses \u2000, which is wider than a space. 209 buf.WriteString(s + "__" + usage + "__") 210 } 211 212 // Write the description if there's any. 213 if cmd.Description != "" { 214 buf.WriteString(": " + cmd.Description) 215 } 216 217 // Add a new line if this isn't the last command. 218 if i != len(sub.Commands)-1 { 219 buf.WriteByte('\n') 220 } 221 } 222 223 return buf.String() 224 } 225 226 // Hide marks a command as hidden, meaning it won't be shown in help and its 227 // UnknownCommand errors will be suppressed. 228 func (sub *Subcommand) Hide(methodName string) { 229 sub.FindCommand(methodName).Hidden = true 230 } 231 232 func (sub *Subcommand) reflectCommands() error { 233 t := reflect.TypeOf(sub.command) 234 v := reflect.ValueOf(sub.command) 235 236 if t.Kind() != reflect.Ptr { 237 return errors.New("sub is not a pointer") 238 } 239 240 // Set the pointer fields 241 sub.ptrValue = v 242 sub.ptrType = t 243 244 ts := t.Elem() 245 vs := v.Elem() 246 247 if ts.Kind() != reflect.Struct { 248 return errors.New("sub is not pointer to struct") 249 } 250 251 // Set the struct fields 252 sub.cmdValue = vs 253 sub.cmdType = ts 254 255 return nil 256 } 257 258 // InitCommands fills a Subcommand with a context. This shouldn't be called at 259 // all, rather you should use the RegisterSubcommand method of a Context. 260 func (sub *Subcommand) InitCommands(ctx *Context) error { 261 // Start filling up a *Context field 262 if err := sub.fillStruct(ctx); err != nil { 263 return err 264 } 265 266 // See if struct implements CanSetup: 267 if v, ok := sub.command.(CanSetup); ok { 268 v.Setup(sub) 269 } 270 271 // See if struct implements CanHelper: 272 if v, ok := sub.command.(CanHelp); ok { 273 sub.helper = v.Help 274 } 275 276 return nil 277 } 278 279 func (sub *Subcommand) fillStruct(ctx *Context) error { 280 for i := 0; i < sub.cmdValue.NumField(); i++ { 281 field := sub.cmdValue.Field(i) 282 283 if !field.CanSet() || !field.CanInterface() { 284 continue 285 } 286 287 if _, ok := field.Interface().(*Context); !ok { 288 continue 289 } 290 291 field.Set(reflect.ValueOf(ctx)) 292 return nil 293 } 294 295 return errors.New("no fields with *bot.Context found") 296 } 297 298 func (sub *Subcommand) parseCommands() error { 299 var numMethods = sub.ptrValue.NumMethod() 300 301 for i := 0; i < numMethods; i++ { 302 method := sub.ptrValue.Method(i) 303 304 if !method.CanInterface() { 305 continue 306 } 307 308 methodT := sub.ptrType.Method(i) 309 if methodT.Name == "Setup" && methodT.Type == typeSetupFn { 310 continue 311 } 312 313 cctx := parseMethod(method, methodT) 314 if cctx == nil { 315 continue 316 } 317 318 // Append. 319 if cctx.event == typeMessageCreate { 320 sub.Commands = append(sub.Commands, cctx) 321 } else { 322 sub.Events = append(sub.Events, cctx) 323 } 324 } 325 326 return nil 327 } 328 329 // AddMiddleware adds a middleware into multiple or all methods, including 330 // commands and events. Multiple method names can be comma-delimited. For all 331 // methods, use a star (*). The given middleware argument can either be a 332 // function with one of the allowed methods or a *MiddlewareContext. 333 // 334 // Allowed function signatures 335 // 336 // Below are the acceptable function signatures that would be parsed as a proper 337 // middleware. A return value of type T will be ignored. If the given function 338 // is invalid, then this method will panic. 339 // 340 // func(<AnyEvent>) (T, error) 341 // func(<AnyEvent>) error 342 // func(<AnyEvent>) 343 // 344 // Note that although technically all of the above function signatures are 345 // acceptable, one should almost always return only an error. 346 func (sub *Subcommand) AddMiddleware(methodName string, middleware interface{}) { 347 var mw *MiddlewareContext 348 // Allow *MiddlewareContext to be passed into. 349 if v, ok := middleware.(*MiddlewareContext); ok { 350 mw = v 351 } else { 352 mw = ParseMiddleware(middleware) 353 } 354 355 // Parse method name: 356 for _, method := range strings.Split(methodName, ",") { 357 // Trim space. 358 if method = strings.TrimSpace(method); method == "*" { 359 // Append middleware to global middleware slice. 360 sub.globalmws = append(sub.globalmws, mw) 361 continue 362 } 363 // Append middleware to that individual function. 364 sub.findMethod(method).addMiddleware(mw) 365 } 366 } 367 368 func (sub *Subcommand) findMethod(name string) *MethodContext { 369 for _, ev := range sub.Events { 370 if ev.MethodName == name { 371 return ev 372 } 373 } 374 return sub.FindCommand(name) 375 } 376 377 func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) { 378 // Search for global middlewares. 379 for _, mw := range sub.globalmws { 380 if mw.isEvent(evT) { 381 callers = append(callers, mw) 382 } 383 } 384 385 // Search for specific handlers. 386 for _, cctx := range sub.Events { 387 // We only take middlewares and callers if the event matches and is not 388 // a MessageCreate. The other function already handles that. 389 if cctx.isEvent(evT) { 390 // Add the command's middlewares first. 391 for _, mw := range cctx.middlewares { 392 // Concrete struct to interface conversion done implicitly. 393 callers = append(callers, mw) 394 } 395 396 callers = append(callers, cctx) 397 } 398 } 399 return 400 } 401 402 // SetPlumb sets the method as the plumbed command. 403 func (sub *Subcommand) SetPlumb(methodName string) { 404 sub.plumbed = sub.FindCommand(methodName) 405 } 406 407 // AddAliases add alias(es) to specific command (defined with commandName). 408 func (sub *Subcommand) AddAliases(commandName string, aliases ...string) { 409 // Get command 410 command := sub.FindCommand(commandName) 411 412 // Write new listing of aliases 413 command.Aliases = append(command.Aliases, aliases...) 414 } 415 416 func lowerFirstLetter(name string) string { 417 return strings.ToLower(string(name[0])) + name[1:] 418 }