diff --git a/_example/advanced_bot/context.go b/_example/advanced_bot/bot.go similarity index 78% rename from _example/advanced_bot/context.go rename to _example/advanced_bot/bot.go index 13d373f..3805380 100644 --- a/_example/advanced_bot/context.go +++ b/_example/advanced_bot/bot.go @@ -19,11 +19,13 @@ type Bot struct { Ctx *bot.Context } +// Help prints the default help message. func (bot *Bot) Help(m *gateway.MessageCreateEvent) error { _, err := bot.Ctx.SendMessage(m.ChannelID, bot.Ctx.Help(), nil) return err } +// Add demonstrates the usage of typed arguments. Run it with "~add 1 2". func (bot *Bot) Add(m *gateway.MessageCreateEvent, a, b int) error { content := fmt.Sprintf("%d + %d = %d", a, b, a+b) @@ -31,11 +33,13 @@ func (bot *Bot) Add(m *gateway.MessageCreateEvent, a, b int) error { return err } +// Ping is a simple ping example, perhaps the most simple you could make it. func (bot *Bot) Ping(m *gateway.MessageCreateEvent) error { _, err := bot.Ctx.SendMessage(m.ChannelID, "Pong!", nil) return err } +// Say demonstrates how arguments.Flag could be used without the flag library. func (bot *Bot) Say(m *gateway.MessageCreateEvent, f *arguments.Flag) error { args := f.String() if args == "" { @@ -47,6 +51,22 @@ func (bot *Bot) Say(m *gateway.MessageCreateEvent, f *arguments.Flag) error { return err } +// GuildInfo demonstrates the use of command flags, in this case the GuildOnly +// flag. +func (bot *Bot) GーGuildInfo(m *gateway.MessageCreateEvent) error { + g, err := bot.Ctx.Guild(m.GuildID) + if err != nil { + return fmt.Errorf("Failed to get guild: %v", err) + } + + _, err = bot.Ctx.SendMessage(m.ChannelID, fmt.Sprintf( + "Your guild is %s, and its maximum members is %d", + g.Name, g.MaxMembers, + ), nil) + + return err +} + // Repeat tells the bot to wait for the user's response, then repeat what they // said. func (bot *Bot) Repeat(m *gateway.MessageCreateEvent) error { @@ -80,6 +100,8 @@ func (bot *Bot) Repeat(m *gateway.MessageCreateEvent) error { return err } +// Embed is a simple embed creator. Its purpose is to demonstrate the usage of +// the ParseContent interface, as well as using the stdlib flag package. func (bot *Bot) Embed( m *gateway.MessageCreateEvent, f *arguments.Flag) error { diff --git a/_example/advanced_bot/debug.go b/_example/advanced_bot/debug.go new file mode 100644 index 0000000..09ba03c --- /dev/null +++ b/_example/advanced_bot/debug.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "log" + "runtime" + "strings" + + "github.com/diamondburned/arikawa/bot" + "github.com/diamondburned/arikawa/gateway" +) + +// Flag for administrators only. +type Debug struct { + Context *bot.Context +} + +// Setup demonstrates the CanSetup interface. This function will never be parsed +// as a callback of any event. +func (d *Debug) Setup(sub *bot.Subcommand) { + // Set a custom command (e.g. "!go ..."): + sub.Command = "go" + // Set a custom description: + sub.Description = "Print Go debugging variables" + + // Manually set the usage for each function. + + sub.ChangeCommandInfo("GOOS", "", + "Prints the current operating system") + + sub.ChangeCommandInfo("GC", "", + "Triggers the garbage collecto") + + sub.ChangeCommandInfo("Goroutines", "", + "Prints the current number of Goroutines") +} + +// ~go goroutines +func (d *Debug) Goroutines(m *gateway.MessageCreateEvent) error { + _, err := d.Context.SendMessage(m.ChannelID, fmt.Sprintf( + "goroutines: %d", + runtime.NumGoroutine(), + ), nil) + return err +} + +// ~go GOOS +func (d *Debug) RーGOOS(m *gateway.MessageCreateEvent) error { + _, err := d.Context.SendMessage( + m.ChannelID, strings.Title(runtime.GOOS), nil) + return err +} + +// ~go GC +func (d *Debug) RーGC(m *gateway.MessageCreateEvent) error { + runtime.GC() + + _, err := d.Context.SendMessage(m.ChannelID, "Done.", nil) + return err +} + +// ~go die +// This command will be hidden from ~help by default. +func (d *Debug) AーDie(m *gateway.MessageCreateEvent) error { + log.Fatalln("User", m.Author.Username, "killed the bot x_x") + return nil +} diff --git a/_example/advanced_bot/main.go b/_example/advanced_bot/main.go index 4121e71..35b887b 100644 --- a/_example/advanced_bot/main.go +++ b/_example/advanced_bot/main.go @@ -19,6 +19,10 @@ func main() { stop, err := bot.Start(token, commands, func(ctx *bot.Context) error { ctx.Prefix = "!" + + // Subcommand demo, but this can be in another package. + ctx.MustRegisterSubcommand(&Debug{}) + return nil }) diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 0000000..deda565 --- /dev/null +++ b/bot/README.md @@ -0,0 +1,4 @@ +# What happened here? + +We've moved everything to https://github.com/diamondburned/ak-rfrouter, as this +package will be replaced with a [go-chi](https://github.com/go-chi/chi) style router. diff --git a/bot/arguments.go b/bot/arguments.go index 364d4c1..042546e 100644 --- a/bot/arguments.go +++ b/bot/arguments.go @@ -23,6 +23,8 @@ type ManualParseable interface { ParseContent([]string) error } +// RawArguments implements ManualParseable, in case you want to implement a +// custom argument parser. It borrows the library's argument parser. type RawArguments struct { Arguments []string } @@ -32,6 +34,13 @@ func (r *RawArguments) ParseContent(args []string) error { return nil } +// Argument is each argument in a method. +type Argument struct { + String string + Type reflect.Type + fn argumentValueFn +} + // nilV, only used to return an error var nilV = reflect.Value{} diff --git a/bot/ctx.go b/bot/ctx.go index 742004c..336837a 100644 --- a/bot/ctx.go +++ b/bot/ctx.go @@ -5,6 +5,7 @@ import ( "os" "os/signal" "strings" + "sync" "github.com/diamondburned/arikawa/gateway" "github.com/diamondburned/arikawa/state" @@ -55,8 +56,13 @@ type Context struct { // ReplyError when true replies to the user the error. ReplyError bool - // Subcommands contains all the registered subcommands. - Subcommands []*Subcommand + // Subcommands contains all the registered subcommands. This is not + // exported, as it shouldn't be used directly. + subcommands []*Subcommand + + // Quick access map from event types to pointers. This map will never have + // MessageCreateEvent's type. + typeCache sync.Map // map[reflect.Type][]*CommandContext } // Start quickly starts a bot with the given command. It will prepend "Bot" @@ -143,6 +149,50 @@ func New(s *state.State, cmd interface{}) (*Context, error) { return ctx, nil } +func (ctx *Context) Subcommands() []*Subcommand { + // Getter is not useless, refer to the struct doc for reason. + return ctx.subcommands +} + +// FindCommand finds a command based on the struct and method name. The queried +// names will have their flags stripped. +// +// Example +// +// // Find a command from the main context: +// cmd := ctx.FindCommand("", "Method") +// // Find a command from a subcommand: +// cmd = ctx.FindCommand("Starboard", "Reset") +// +func (ctx *Context) FindCommand(structname, methodname string) *CommandContext { + if structname == "" { + for _, c := range ctx.Commands { + if c.Command == methodname { + return c + } + } + + return nil + } + + for _, sub := range ctx.subcommands { + if sub.StructName != structname { + continue + } + + for _, c := range sub.Commands { + if c.Command == methodname { + return c + } + } + } + + return nil +} + +// MustRegisterSubcommand tries to register a subcommand, and will panic if it +// fails. This is recommended, as subcommands won't change after initializing +// once in runtime, thus fairly harmless after development. func (ctx *Context) MustRegisterSubcommand(cmd interface{}) *Subcommand { s, err := ctx.RegisterSubcommand(cmd) if err != nil { @@ -168,14 +218,14 @@ func (ctx *Context) RegisterSubcommand(cmd interface{}) (*Subcommand, error) { } // Do a collision check - for _, sub := range ctx.Subcommands { - if sub.name == s.name { + for _, sub := range ctx.subcommands { + if sub.Command == s.Command { return nil, errors.New( - "New subcommand has duplicate name: " + s.name) + "New subcommand has duplicate name: " + s.Command) } } - ctx.Subcommands = append(ctx.Subcommands, s) + ctx.subcommands = append(ctx.subcommands, s) return s, nil } @@ -184,27 +234,33 @@ func (ctx *Context) RegisterSubcommand(cmd interface{}) (*Subcommand, error) { // Session handlers. func (ctx *Context) Start() func() { return ctx.Session.AddHandler(func(v interface{}) { - if err := ctx.callCmd(v); err != nil { - if str := ctx.FormatError(err); str != "" { - // Log the main error first - if !ctx.ReplyError { - ctx.ErrorLogger(errors.Wrap(err, "Command error")) - } + err := ctx.callCmd(v) + if err == nil { + return + } - mc, ok := v.(*gateway.MessageCreateEvent) - if !ok { - return - } + str := ctx.FormatError(err) + if str == "" { + return + } - if ctx.ReplyError { - _, Merr := ctx.SendMessage(mc.ChannelID, str, nil) - if Merr != nil { - // Then the message error - ctx.ErrorLogger(Merr) - // TODO: there ought to be a better way lol - } - } - } + // Log the main error first... + if !ctx.ReplyError { + ctx.ErrorLogger(errors.Wrap(err, "Command error")) + return + } + + mc, ok := v.(*gateway.MessageCreateEvent) + if !ok { + return + } + + _, err = ctx.SendMessage(mc.ChannelID, str, nil) + if err != nil { + // ...then the message error + ctx.ErrorLogger(err) + + // TODO: there ought to be a better way lol } }) } @@ -247,7 +303,7 @@ func (ctx *Context) Help() string { continue } - help.WriteString(" " + ctx.Prefix + cmd.Name()) + help.WriteString(" " + ctx.Prefix + cmd.Command) switch { case len(cmd.Usage()) > 0: @@ -260,14 +316,15 @@ func (ctx *Context) Help() string { } var subHelp = strings.Builder{} + var subcommands = ctx.Subcommands() - for _, sub := range ctx.Subcommands { + for _, sub := range subcommands { if sub.Flag.Is(AdminOnly) { // Hidden continue } - subHelp.WriteString(" " + sub.Name()) + subHelp.WriteString(" " + sub.Command) if sub.Description != "" { subHelp.WriteString(": " + sub.Description) @@ -281,7 +338,7 @@ func (ctx *Context) Help() string { } subHelp.WriteString(" " + - ctx.Prefix + sub.Name() + " " + cmd.Name()) + ctx.Prefix + sub.Command + " " + cmd.Command) switch { case len(cmd.Usage()) > 0: diff --git a/bot/ctx_call.go b/bot/ctx_call.go index 787ec9d..a945ee8 100644 --- a/bot/ctx_call.go +++ b/bot/ctx_call.go @@ -9,57 +9,102 @@ import ( "github.com/diamondburned/arikawa/gateway" ) -func (ctx *Context) filter( - check func(sub *Subcommand, cmd *CommandContext) bool) []reflect.Value { - - var callers []reflect.Value +func (ctx *Context) filterEventType(evT reflect.Type) []*CommandContext { + var callers []*CommandContext + var middles []*CommandContext + var found bool for _, cmd := range ctx.Commands { - if check(nil, cmd) { - callers = append(callers, cmd.value) + // Inherit parent's flags + cmd.Flag |= ctx.Flag + + // Check if middleware + if cmd.Flag.Is(Middleware) { + continue + } + + if cmd.event == evT { + callers = append(callers, cmd) + found = true } } - for _, sub := range ctx.Subcommands { + if found { + middles = append(middles, ctx.mwMethods...) + } + + for _, sub := range ctx.subcommands { + // Reset found status + found = false + for _, cmd := range sub.Commands { - if check(sub, cmd) { - callers = append(callers, cmd.value) + // Inherit parent's flags + cmd.Flag |= sub.Flag + + // Check if middleware + if cmd.Flag.Is(Middleware) { + continue + } + + if cmd.event == evT { + callers = append(callers, cmd) + found = true } } + + if found { + middles = append(middles, sub.mwMethods...) + } } - return callers + return append(middles, callers...) } func (ctx *Context) callCmd(ev interface{}) error { evT := reflect.TypeOf(ev) - if evT != typeMessageCreate { - var isAdmin *bool // i want to die - var isGuild *bool - - callers := ctx.filter(func(sub *Subcommand, cmd *CommandContext) bool { - if sub != nil { - cmd.Flag |= sub.Flag - } - - return true && - !(cmd.Flag.Is(AdminOnly) && !ctx.eventIsAdmin(ev, &isAdmin)) && - !(cmd.Flag.Is(GuildOnly) && !ctx.eventIsGuild(ev, &isGuild)) - }) - - for _, c := range callers { - if err := callWith(c, ev); err != nil { - ctx.ErrorLogger(err) - } - } - - return nil + if evT == typeMessageCreate { + // safe assertion always + return ctx.callMessageCreate(ev.(*gateway.MessageCreateEvent)) } - // safe assertion always - mc := ev.(*gateway.MessageCreateEvent) + var isAdmin *bool // I want to die. + var isGuild *bool + var callers []*CommandContext + // Hit the cache + t, ok := ctx.typeCache.Load(evT) + if ok { + callers = t.([]*CommandContext) + } else { + callers = ctx.filterEventType(evT) + ctx.typeCache.Store(evT, callers) + } + + // We can't do the callers[:0] trick here, as it will modify the slice + // inside the sync.Map as well. + var filtered = make([]*CommandContext, 0, len(callers)) + + for _, cmd := range callers { + // Command flags will inherit its parent Subcommand's flags. + if true && + !(cmd.Flag.Is(AdminOnly) && !ctx.eventIsAdmin(ev, &isAdmin)) && + !(cmd.Flag.Is(GuildOnly) && !ctx.eventIsGuild(ev, &isGuild)) { + + filtered = append(filtered, cmd) + } + } + + for _, c := range filtered { + if err := callWith(c.value, ev); err != nil { + ctx.ErrorLogger(err) + } + } + + return nil +} + +func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent) error { // check if prefix if !strings.HasPrefix(mc.Content, ctx.Prefix) { // not a command, ignore @@ -68,6 +113,7 @@ func (ctx *Context) callCmd(ev interface{}) error { // trim the prefix before splitting, this way multi-words prefices work content := mc.Content[len(ctx.Prefix):] + content = strings.TrimSpace(content) if content == "" { return nil // just the prefix only @@ -84,12 +130,14 @@ func (ctx *Context) callCmd(ev interface{}) error { } var cmd *CommandContext + var sub *Subcommand var start int // arg starts from $start // Search for the command for _, c := range ctx.Commands { - if c.name == args[0] { + if c.Command == args[0] { cmd = c + sub = ctx.Subcommand start = 1 break } @@ -99,14 +147,15 @@ func (ctx *Context) callCmd(ev interface{}) error { // entry. if cmd == nil && len(args) > 1 { SubcommandLoop: - for _, s := range ctx.Subcommands { - if s.name != args[0] { + for _, s := range ctx.subcommands { + if s.Command != args[0] { continue } for _, c := range s.Commands { - if c.name == args[1] { + if c.Command == args[1] { cmd = c + sub = s start = 2 // OR the flags @@ -182,12 +231,12 @@ func (ctx *Context) callCmd(ev interface{}) error { // Here's an edge case: when the handler takes no arguments, we allow that // anyway, as they might've used the raw content. - if len(cmd.arguments) == 0 { + if len(cmd.Arguments) == 0 { goto Call } // Not enough arguments given - if len(args[start:]) != len(cmd.arguments) { + if len(args[start:]) != len(cmd.Arguments) { return &ErrInvalidUsage{ Args: args, Prefix: ctx.Prefix, @@ -197,10 +246,10 @@ func (ctx *Context) callCmd(ev interface{}) error { } } - argv = make([]reflect.Value, len(cmd.arguments)) + argv = make([]reflect.Value, len(cmd.Arguments)) for i := start; i < len(args); i++ { - v, err := cmd.arguments[i-start](args[i]) + v, err := cmd.Arguments[i-start].fn(args[i]) if err != nil { return &ErrInvalidUsage{ Args: args, @@ -215,8 +264,16 @@ func (ctx *Context) callCmd(ev interface{}) error { } Call: + // Try calling all middlewares first. We don't need to stack middlewares, as + // there will only be one command match. + for _, mw := range sub.mwMethods { + if err := callWith(mw.value, mc); err != nil { + return err + } + } + // call the function and parse the error return value - return callWith(cmd.value, ev, argv...) + return callWith(cmd.value, mc, argv...) } func (ctx *Context) eventIsAdmin(ev interface{}, is **bool) bool { diff --git a/bot/ctx_test.go b/bot/ctx_test.go index 5e7c467..4f777dd 100644 --- a/bot/ctx_test.go +++ b/bot/ctx_test.go @@ -4,6 +4,7 @@ package bot import ( "reflect" + "strconv" "strings" "testing" @@ -14,8 +15,19 @@ import ( ) type testCommands struct { - Ctx *Context - Return chan interface{} + Ctx *Context + Return chan interface{} + Counter uint64 +} + +func (t *testCommands) MーBumpCounter(interface{}) error { + t.Counter++ + return nil +} + +func (t *testCommands) GetCounter(*gateway.MessageCreateEvent) error { + t.Return <- strconv.FormatUint(t.Counter, 10) + return nil } func (t *testCommands) Send(_ *gateway.MessageCreateEvent, arg string) error { @@ -118,12 +130,21 @@ func TestContext(t *testing.T) { return } + t.Run("middleware", func(t *testing.T) { + ctx.Prefix = "pls do" + + // This should trigger the middleware first. + if err := testReturn("1", "pls do getcounter"); err != nil { + t.Fatal("Unexpected error:", err) + } + }) + t.Run("call command", func(t *testing.T) { // Set a custom prefix ctx.Prefix = "~" if err := testReturn("test", "~send test"); err.Error() != "oh no" { - t.Fatal("unexpected error:", err) + t.Fatal("Unexpected error:", err) } }) diff --git a/bot/extras/arguments/flag.go b/bot/extras/arguments/flag.go index f99ab7b..a0ad7f6 100644 --- a/bot/extras/arguments/flag.go +++ b/bot/extras/arguments/flag.go @@ -31,12 +31,13 @@ func (fs *FlagSet) Usage() string { } type Flag struct { + command string arguments []string } func (f *Flag) ParseContent(arguments []string) error { // trim the command out - f.arguments = arguments[1:] + f.command, f.arguments = arguments[0], arguments[1:] return nil } @@ -44,6 +45,10 @@ func (f *Flag) Usage() string { return "flags..." } +func (f *Flag) Command() string { + return f.command +} + func (f *Flag) Args() []string { return f.arguments } diff --git a/bot/nameflag.go b/bot/nameflag.go index 1274605..69f379b 100644 --- a/bot/nameflag.go +++ b/bot/nameflag.go @@ -9,11 +9,34 @@ const FlagSeparator = 'ー' const ( None NameFlag = 1 << iota - // These flags only apply to messageCreate events. + // !!! + // + // These flags are applied to all events, if possible. The defined behavior + // is to search for "ChannelID" fields or "ID" fields in structs with + // "Channel" in its name. It doesn't handle individual events, as such, will + // not be able to guarantee it will always work. - Raw // R - AdminOnly // A - GuildOnly // G + // R - Raw, which tells the library to use the method name as-is (flags will + // still be stripped). For example, if a method is called Reset its + // command will also be Reset, without being all lower-cased. + Raw + + // A - AdminOnly, which tells the library to only run the Subcommand/method + // if the user is admin or not. This will automatically add GuildOnly as + // well. + AdminOnly + + // G - GuildOnly, which tells the library to only run the Subcommand/method + // if the user is inside a guild. + GuildOnly + + // M - Middleware, which tells the library that the method is a middleware. + // The method will be executed anytime a method of the same struct is + // matched. + // + // Using this flag inside the subcommand will drop all methods (this is an + // undefined behavior/UB). + Middleware ) func ParseFlag(name string) (NameFlag, string) { @@ -32,6 +55,8 @@ func ParseFlag(name string) (NameFlag, string) { f |= AdminOnly | GuildOnly case 'G': f |= GuildOnly + case 'M': + f |= Middleware } } diff --git a/bot/subcommand.go b/bot/subcommand.go index 8c6cf66..0b889f8 100644 --- a/bot/subcommand.go +++ b/bot/subcommand.go @@ -10,21 +10,34 @@ import ( var ( typeMessageCreate = reflect.TypeOf((*gateway.MessageCreateEvent)(nil)) - // typeof.Implements(typeI*) + + typeSubcmd = reflect.TypeOf((*Subcommand)(nil)) + typeIError = reflect.TypeOf((*error)(nil)).Elem() typeIManP = reflect.TypeOf((*ManualParseable)(nil)).Elem() typeIParser = reflect.TypeOf((*Parseable)(nil)).Elem() - typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem() + typeSetupFn = func() reflect.Type { + method, _ := reflect.TypeOf((*CanSetup)(nil)). + Elem(). + MethodByName("Setup") + return method.Type + }() ) type Subcommand struct { Description string - // Commands contains all the registered command contexts. + // Raw struct name, including the flag (only filled for actual subcommands, + // will be empty for Context): + StructName string + // Parsed command name: + Command string + + // All registered command contexts: Commands []*CommandContext - // struct name - name string + // Middleware command contexts: + mwMethods []*CommandContext // struct flags Flag NameFlag @@ -48,14 +61,14 @@ type CommandContext struct { Description string Flag NameFlag - name string // all lower-case + MethodName string + Command string + value reflect.Value // Func event reflect.Type // gateway.*Event method reflect.Method - // equal slices - argStrings []string - arguments []argumentValueFn + Arguments []Argument // only for ParseContent interface parseMethod reflect.Method @@ -63,24 +76,12 @@ type CommandContext struct { parseUsage string } -// Descriptor is optionally used to set the Description of a command context. -type Descriptor interface { - Description() string -} - -// Namer is optionally used to override the command context's name. -type Namer interface { - Name() string -} - -// Usager is optionally used to override the generated usage for either an -// argument, or multiple (using ManualParseable). -type Usager interface { - Usage() string -} - -func (cctx *CommandContext) Name() string { - return cctx.name +// CanSetup is used for subcommands to change variables, such as Description. +// This method will be triggered when InitCommands is called, which is during +// New for Context and during RegisterSubcommand for subcommands. +type CanSetup interface { + // Setup should panic when it has an error. + Setup(*Subcommand) } func (cctx *CommandContext) Usage() []string { @@ -88,11 +89,16 @@ func (cctx *CommandContext) Usage() []string { return []string{cctx.parseUsage} } - if len(cctx.arguments) == 0 { + if len(cctx.Arguments) == 0 { return nil } - return cctx.argStrings + var arguments = make([]string, len(cctx.Arguments)) + for i, arg := range cctx.Arguments { + arguments[i] = arg.String + } + + return arguments } func NewSubcommand(cmd interface{}) (*Subcommand, error) { @@ -100,11 +106,6 @@ func NewSubcommand(cmd interface{}) (*Subcommand, error) { command: cmd, } - // Set description - if d, ok := cmd.(Descriptor); ok { - sub.Description = d.Description() - } - if err := sub.reflectCommands(); err != nil { return nil, errors.Wrap(err, "Failed to reflect commands") } @@ -116,30 +117,42 @@ func NewSubcommand(cmd interface{}) (*Subcommand, error) { return &sub, nil } -// Name returns the command name in lower case. This only returns non-zero for -// subcommands. -func (sub *Subcommand) Name() string { - return sub.name -} - // NeedsName sets the name for this subcommand. Like InitCommands, this // shouldn't be called at all, rather you should use RegisterSubcommand. func (sub *Subcommand) NeedsName() { - flag, name := ParseFlag(sub.cmdType.Name()) + sub.StructName = sub.cmdType.Name() - // Check for interface - if n, ok := sub.command.(Namer); ok { - name = n.Name() - } + flag, name := ParseFlag(sub.StructName) if !flag.Is(Raw) { name = strings.ToLower(name) } - sub.name = name + sub.Command = name sub.Flag = flag } +// ChangeCommandInfo changes the matched methodName's Command and Description. +// Empty means unchanged. The returned bool is true when the method is found. +func (sub *Subcommand) ChangeCommandInfo(methodName, cmd, desc string) bool { + for _, c := range sub.Commands { + if c.MethodName != methodName { + continue + } + + if cmd != "" { + c.Command = cmd + } + if desc != "" { + c.Description = desc + } + + return true + } + + return false +} + func (sub *Subcommand) reflectCommands() error { t := reflect.TypeOf(sub.command) v := reflect.ValueOf(sub.command) @@ -170,6 +183,19 @@ func (sub *Subcommand) reflectCommands() error { // all, rather you should use the RegisterSubcommand method of a Context. func (sub *Subcommand) InitCommands(ctx *Context) error { // Start filling up a *Context field + if err := sub.fillStruct(ctx); err != nil { + return err + } + + // See if struct implements CanSetup: + if v, ok := sub.command.(CanSetup); ok { + v.Setup(sub) + } + + return nil +} + +func (sub *Subcommand) fillStruct(ctx *Context) error { for i := 0; i < sub.cmdValue.NumField(); i++ { field := sub.cmdValue.Field(i) @@ -202,8 +228,18 @@ func (sub *Subcommand) parseCommands() error { methodT := method.Type() numArgs := methodT.NumIn() - // Doesn't meet requirement for an event if numArgs == 0 { + // Doesn't meet the requirement for an event, continue. + continue + } + + if methodT == typeSetupFn { + // Method is a setup method, continue. + continue + } + + // Check number of returns: + if methodT.NumOut() != 1 { continue } @@ -222,40 +258,44 @@ func (sub *Subcommand) parseCommands() error { // Parse the method name flag, name := ParseFlag(command.method.Name) - if !flag.Is(Raw) { - name = strings.ToLower(name) - } - - // Set the method name and flag - command.name = name + // Set the method name, command, and flag: + command.MethodName = name + command.Command = name command.Flag = flag + // Check if Raw is enabled for command: + if !flag.Is(Raw) { + command.Command = strings.ToLower(name) + } + // TODO: allow more flexibility if command.event != typeMessageCreate { goto Done } + // If the method only takes an event: if numArgs == 1 { // done goto Done } + // Middlewares shouldn't even have arguments. + if flag.Is(Middleware) { + goto Done + } + // If the second argument implements ParseContent() if t := methodT.In(1); t.Implements(typeIManP) { mt, _ := t.MethodByName("ParseContent") command.parseMethod = mt command.parseType = t.Elem() - - command.parseUsage = usager(t) - if command.parseUsage == "" { - command.parseUsage = t.String() - } + command.parseUsage = t.String() goto Done } - command.arguments = make([]argumentValueFn, 0, numArgs) + command.Arguments = make([]Argument, 0, numArgs) // Fill up arguments for i := 1; i < numArgs; i++ { @@ -266,33 +306,22 @@ func (sub *Subcommand) parseCommands() error { return errors.Wrap(err, "Error parsing argument "+t.String()) } - command.arguments = append(command.arguments, avfs) - - var usage = usager(t) - if usage == "" { - usage = t.String() - } - - command.argStrings = append(command.argStrings, usage) + command.Arguments = append(command.Arguments, Argument{ + String: t.String(), + Type: t, + fn: avfs, + }) } Done: // Append - commands = append(commands, &command) + if flag.Is(Middleware) { + sub.mwMethods = append(sub.mwMethods, &command) + } else { + commands = append(commands, &command) + } } sub.Commands = commands return nil } - -func usager(t reflect.Type) string { - if !t.Implements(typeIUsager) { - return "" - } - - usageFn, _ := t.MethodByName("Usage") - v := usageFn.Func.Call([]reflect.Value{ - reflect.New(t.Elem()), - }) - return v[0].String() -} diff --git a/bot/subcommand_test.go b/bot/subcommand_test.go index 7c5f655..f83c10b 100644 --- a/bot/subcommand_test.go +++ b/bot/subcommand_test.go @@ -29,7 +29,7 @@ func TestSubcommand(t *testing.T) { } // !!! CHANGE ME - if len(sub.Commands) != 4 { + if len(sub.Commands) != 5 { t.Fatal("invalid ctx.commands len", len(sub.Commands)) } @@ -40,7 +40,7 @@ func TestSubcommand(t *testing.T) { ) for _, this := range sub.Commands { - switch this.name { + switch this.Command { case "send": foundSend = true if len(this.arguments) != 1 { @@ -65,11 +65,11 @@ func TestSubcommand(t *testing.T) { t.Fatal("unexpected parseType") } - case "noop": + case "noop", "getcounter": // Found, but whatever default: - t.Fatal("Unexpected command:", this.name) + t.Fatal("Unexpected command:", this.Command) } if this.event != typeMessageCreate {