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:
parent
09d8c5bc43
commit
aadcbd0767
|
@ -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 {
|
||||
|
67
_example/advanced_bot/debug.go
Normal file
67
_example/advanced_bot/debug.go
Normal 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
|
||||
}
|
|
@ -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
4
bot/README.md
Normal 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.
|
|
@ -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{}
|
||||
|
||||
|
|
115
bot/ctx.go
115
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:
|
||||
|
|
141
bot/ctx_call.go
141
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue