mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-12-02 20:02:53 +00:00
528 lines
12 KiB
Go
528 lines
12 KiB
Go
package bot
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/diamondburned/arikawa/api"
|
|
"github.com/diamondburned/arikawa/bot/extras/infer"
|
|
"github.com/diamondburned/arikawa/discord"
|
|
"github.com/diamondburned/arikawa/gateway"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// NonFatal is an interface that a method can implement to ignore all errors.
|
|
// This works similarly to Break.
|
|
type NonFatal interface {
|
|
error
|
|
IgnoreError() // noop method
|
|
}
|
|
|
|
func onlyFatal(err error) error {
|
|
if _, ok := err.(NonFatal); ok {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
type _Break struct{ error }
|
|
|
|
// implement NonFatal.
|
|
func (_Break) IgnoreError() {}
|
|
|
|
// Break is a non-fatal error that could be returned from middlewares or
|
|
// handlers to stop the chain of execution.
|
|
//
|
|
// Middlewares are guaranteed to be executed before handlers, but the exact
|
|
// order of each are undefined. Main handlers are also guaranteed to be executed
|
|
// before all subcommands. If a main middleware cancels, no subcommand
|
|
// middlewares will be called.
|
|
//
|
|
// Break implements the NonFatal interface, which causes an error to be ignored.
|
|
var Break NonFatal = _Break{errors.New("break middleware chain, non-fatal")}
|
|
|
|
func (ctx *Context) filterEventType(evT reflect.Type) []*CommandContext {
|
|
var callers []*CommandContext
|
|
var middles []*CommandContext
|
|
var found bool
|
|
|
|
find := func(sub *Subcommand) {
|
|
for _, cmd := range sub.Events {
|
|
// Search only for callers, so skip middlewares.
|
|
if cmd.Flag.Is(Middleware) {
|
|
continue
|
|
}
|
|
|
|
if cmd.event == evT {
|
|
callers = append(callers, cmd)
|
|
found = true
|
|
}
|
|
}
|
|
|
|
// Only get middlewares if we found handlers for that same event.
|
|
if found {
|
|
// Search for middlewares with the same type:
|
|
for _, mw := range sub.mwMethods {
|
|
if mw.event == evT {
|
|
middles = append(middles, mw)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the main context first.
|
|
find(ctx.Subcommand)
|
|
|
|
for _, sub := range ctx.subcommands {
|
|
// Reset found status
|
|
found = false
|
|
// Find subcommands second.
|
|
find(sub)
|
|
}
|
|
|
|
return append(middles, callers...)
|
|
}
|
|
|
|
func (ctx *Context) callCmd(ev interface{}) error {
|
|
evT := reflect.TypeOf(ev)
|
|
|
|
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 {
|
|
_, err := callWith(c.value, ev)
|
|
if err != nil {
|
|
if err = onlyFatal(err); err != nil {
|
|
ctx.ErrorLogger(err)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
// We call the messages later, since Hidden handlers will go into the Events
|
|
// slice, but we don't want to ignore those handlers either.
|
|
if evT == typeMessageCreate {
|
|
// safe assertion always
|
|
err := ctx.callMessageCreate(ev.(*gateway.MessageCreateEvent))
|
|
return onlyFatal(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent) error {
|
|
// check if bot
|
|
if !ctx.AllowBot && mc.Author.Bot {
|
|
return nil
|
|
}
|
|
|
|
// check if prefix
|
|
pf, ok := ctx.HasPrefix(mc)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// trim the prefix before splitting, this way multi-words prefices work
|
|
content := mc.Content[len(pf):]
|
|
|
|
if content == "" {
|
|
return nil // just the prefix only
|
|
}
|
|
|
|
// parse arguments
|
|
parts, err := ctx.ParseArgs(content)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to parse command")
|
|
}
|
|
|
|
if len(parts) == 0 {
|
|
return nil // ???
|
|
}
|
|
|
|
var cmd *CommandContext
|
|
var sub *Subcommand
|
|
// var start int // arg starts from $start
|
|
|
|
// Check if plumb:
|
|
if ctx.plumb {
|
|
cmd = ctx.Commands[0]
|
|
sub = ctx.Subcommand
|
|
// start = 0
|
|
}
|
|
|
|
// Arguments slice, which will be sliced away until only arguments are left.
|
|
var arguments = parts
|
|
|
|
// If not plumb, search for the command
|
|
if cmd == nil {
|
|
for _, c := range ctx.Commands {
|
|
if c.Command == parts[0] {
|
|
cmd = c
|
|
sub = ctx.Subcommand
|
|
arguments = arguments[1:]
|
|
// start = 1
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Can't find the command, look for subcommands if len(args) has a 2nd
|
|
// entry.
|
|
if cmd == nil {
|
|
for _, s := range ctx.subcommands {
|
|
if s.Command != parts[0] {
|
|
continue
|
|
}
|
|
|
|
// Check if plumb:
|
|
if s.plumb {
|
|
cmd = s.Commands[0]
|
|
sub = s
|
|
arguments = arguments[1:]
|
|
// start = 1
|
|
break
|
|
}
|
|
|
|
// There's no second argument, so we can only look for Plumbed
|
|
// subcommands.
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
for _, c := range s.Commands {
|
|
if c.Command == parts[1] {
|
|
cmd = c
|
|
sub = s
|
|
arguments = arguments[2:]
|
|
break
|
|
// start = 2
|
|
}
|
|
}
|
|
|
|
if cmd == nil {
|
|
if s.QuietUnknownCommand {
|
|
return nil
|
|
}
|
|
|
|
return &ErrUnknownCommand{
|
|
Command: parts[1],
|
|
Parent: parts[0],
|
|
ctx: s.Commands,
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if cmd == nil {
|
|
if ctx.QuietUnknownCommand {
|
|
return nil
|
|
}
|
|
|
|
return &ErrUnknownCommand{
|
|
Command: parts[0],
|
|
ctx: ctx.Commands,
|
|
}
|
|
}
|
|
|
|
// Check for IsAdmin and IsGuild
|
|
if cmd.Flag.Is(GuildOnly) && !mc.GuildID.Valid() {
|
|
return nil
|
|
}
|
|
if cmd.Flag.Is(AdminOnly) {
|
|
p, err := ctx.State.Permissions(mc.ChannelID, mc.Author.ID)
|
|
if err != nil || !p.Has(discord.PermissionAdministrator) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Start converting
|
|
var argv []reflect.Value
|
|
var argc int
|
|
|
|
// the last argument in the list, not used until set
|
|
var last Argument
|
|
|
|
// 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) < 1 {
|
|
goto Call
|
|
}
|
|
|
|
// Argument count check.
|
|
if argdelta := len(arguments) - len(cmd.Arguments); argdelta != 0 {
|
|
var err error // no err if nil
|
|
|
|
// If the function is variadic, then we can allow the last argument to
|
|
// be empty.
|
|
if cmd.Variadic {
|
|
argdelta++
|
|
}
|
|
|
|
switch {
|
|
// If there aren't enough arguments given.
|
|
case argdelta < 0:
|
|
err = ErrNotEnoughArgs
|
|
|
|
// If there are too many arguments, then check if the command supports
|
|
// variadic arguments. We already did a length check above.
|
|
case argdelta > 0 && !cmd.Variadic:
|
|
// If it's not variadic, then we can't accept it.
|
|
err = ErrTooManyArgs
|
|
}
|
|
|
|
if err != nil {
|
|
return &ErrInvalidUsage{
|
|
Prefix: pf,
|
|
Args: parts,
|
|
Index: len(parts) - 1,
|
|
Wrap: err,
|
|
Ctx: cmd,
|
|
}
|
|
}
|
|
}
|
|
|
|
// The last argument in the arguments slice.
|
|
last = cmd.Arguments[len(cmd.Arguments)-1]
|
|
|
|
// Allocate a new slice the length of function arguments.
|
|
argc = len(cmd.Arguments) - 1 // arg len without last
|
|
argv = make([]reflect.Value, 0, argc) // could be 0
|
|
|
|
// Parse all arguments except for the last one.
|
|
for i := 0; i < argc; i++ {
|
|
v, err := cmd.Arguments[i].fn(arguments[0])
|
|
if err != nil {
|
|
return &ErrInvalidUsage{
|
|
Prefix: pf,
|
|
Args: parts,
|
|
Index: len(parts) - len(arguments) + i,
|
|
Wrap: err,
|
|
Ctx: cmd,
|
|
}
|
|
}
|
|
|
|
// Pop arguments.
|
|
arguments = arguments[1:]
|
|
argv = append(argv, v)
|
|
}
|
|
|
|
// Is this last argument actually a variadic slice? If yes, then it
|
|
// should still have fn normally.
|
|
if last.fn != nil {
|
|
// Allocate a new slice to append into.
|
|
vars := make([]reflect.Value, 0, len(arguments))
|
|
|
|
// Parse the rest with variadic arguments. Go's reflect states that
|
|
// varidic parameters will automatically be copied, which is good.
|
|
for i := 0; len(arguments) > 0; i++ {
|
|
v, err := last.fn(arguments[0])
|
|
if err != nil {
|
|
return &ErrInvalidUsage{
|
|
Prefix: pf,
|
|
Args: parts,
|
|
Index: len(parts) - len(arguments) + i,
|
|
Wrap: err,
|
|
Ctx: cmd,
|
|
}
|
|
}
|
|
|
|
arguments = arguments[1:]
|
|
vars = append(vars, v)
|
|
}
|
|
|
|
argv = append(argv, vars...)
|
|
|
|
} else {
|
|
// Create a zero value instance of this:
|
|
v := reflect.New(last.rtype)
|
|
var err error // return error
|
|
|
|
switch {
|
|
// If the argument wants all arguments:
|
|
case last.manual != nil:
|
|
// Call the manual parse method:
|
|
_, err = callWith(last.manual.Func, v, reflect.ValueOf(arguments))
|
|
|
|
// If the argument wants all arguments in string:
|
|
case last.custom != nil:
|
|
// Manual string seeking is a must here. This is because the string
|
|
// could contain multiple whitespaces, and the parser would not
|
|
// count them.
|
|
var seekTo = cmd.Command
|
|
// If plumbed, then there would only be the subcommand.
|
|
if sub.plumb {
|
|
seekTo = sub.Command
|
|
}
|
|
|
|
// Seek to the string.
|
|
if i := strings.Index(content, seekTo); i > -1 {
|
|
// Seek past the substring.
|
|
i += len(seekTo)
|
|
content = strings.TrimSpace(content[i:])
|
|
}
|
|
|
|
// Call the method with the raw unparsed command:
|
|
_, err = callWith(last.custom.Func, v, reflect.ValueOf(content))
|
|
}
|
|
|
|
// Check the returned error:
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the argument wants a non-pointer:
|
|
if last.pointer {
|
|
v = v.Elem()
|
|
}
|
|
|
|
// Add the argument into argv.
|
|
argv = append(argv, v)
|
|
}
|
|
|
|
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 {
|
|
_, err := callWith(mw.value, mc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// call the function and parse the error return value
|
|
v, err := callWith(cmd.value, mc, argv...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch v := v.(type) {
|
|
case string:
|
|
v = sub.SanitizeMessage(v)
|
|
_, err = ctx.SendMessage(mc.ChannelID, v, nil)
|
|
case *discord.Embed:
|
|
_, err = ctx.SendMessage(mc.ChannelID, "", v)
|
|
case *api.SendMessageData:
|
|
if v.Content != "" {
|
|
v.Content = sub.SanitizeMessage(v.Content)
|
|
}
|
|
_, err = ctx.SendMessageComplex(mc.ChannelID, *v)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (ctx *Context) eventIsAdmin(ev interface{}, is **bool) bool {
|
|
if *is != nil {
|
|
return **is
|
|
}
|
|
|
|
var channelID = infer.ChannelID(ev)
|
|
if !channelID.Valid() {
|
|
return false
|
|
}
|
|
|
|
var userID = infer.UserID(ev)
|
|
if !userID.Valid() {
|
|
return false
|
|
}
|
|
|
|
var res bool
|
|
|
|
p, err := ctx.State.Permissions(channelID, userID)
|
|
if err == nil && p.Has(discord.PermissionAdministrator) {
|
|
res = true
|
|
}
|
|
|
|
*is = &res
|
|
return res
|
|
}
|
|
|
|
func (ctx *Context) eventIsGuild(ev interface{}, is **bool) bool {
|
|
if *is != nil {
|
|
return **is
|
|
}
|
|
|
|
var channelID = infer.ChannelID(ev)
|
|
if !channelID.Valid() {
|
|
return false
|
|
}
|
|
|
|
c, err := ctx.State.Channel(channelID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
res := c.GuildID.Valid()
|
|
*is = &res
|
|
return res
|
|
}
|
|
|
|
func callWith(
|
|
caller reflect.Value,
|
|
ev interface{}, values ...reflect.Value) (interface{}, error) {
|
|
|
|
var callargs = make([]reflect.Value, 0, 1+len(values))
|
|
|
|
if v, ok := ev.(reflect.Value); ok {
|
|
callargs = append(callargs, v)
|
|
} else {
|
|
callargs = append(callargs, reflect.ValueOf(ev))
|
|
}
|
|
|
|
callargs = append(callargs, values...)
|
|
|
|
return errorReturns(caller.Call(callargs))
|
|
}
|
|
|
|
func errorReturns(returns []reflect.Value) (interface{}, error) {
|
|
// Handlers may return nothing.
|
|
if len(returns) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// assume first return is always error, since we checked for this in
|
|
// parseCommands.
|
|
v := returns[len(returns)-1].Interface()
|
|
// If the last return (error) is nil.
|
|
if v == nil {
|
|
// If we only have 1 returns, that return must be the error. The error
|
|
// is nil, so nil is returned.
|
|
if len(returns) == 1 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Return the first argument as-is. The above returns[-1] check assumes
|
|
// 2 return values (T, error), meaning returns[0] is the T value.
|
|
return returns[0].Interface(), nil
|
|
}
|
|
|
|
// Treat the last return as an error.
|
|
return nil, v.(error)
|
|
}
|