1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2025-03-30 13:59:32 +00:00

Added middleware features into package bot. (#3)

* Middleware nameflag
* Completed M-Middleware feature
* Changed Namer/Descriptor API to CanSetup API
This commit is contained in:
diamondburned 2020-01-23 19:17:03 -08:00 committed by GitHub
parent 09d8c5bc43
commit aadcbd0767
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 463 additions and 163 deletions

View file

@ -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 {

View file

@ -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
}

View file

@ -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
})

4
bot/README.md Normal file
View file

@ -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.

View file

@ -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{}

View file

@ -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:

View file

@ -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 {

View file

@ -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)
}
})

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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 {