1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-12-01 03:03:48 +00:00
arikawa/utils/bot/command.go
diamondburned 54cadd2f45 gateway: Refactor for a better concurrent API
This commit refactors the whole package gateway as well as utils/ws
(formerly utils/wsutil) and voice/voicegateway. The new refactor
utilizes a design pattern involving a concurrent loop and an arriving
event channel.

An additional change was made to the way gateway events are typed.
Before, pretty much any type will satisfy a gateway event type, since
the actual type was just interface{}. The new refactor defines a
concrete interface that events can implement:

    type Event interface {
        Op() OpCode
        EventType() EventType
    }

Using this interface, the user can easily add custom gateway events
independently of the library without relying on string maps. This adds a
lot of type safety into the library and makes type-switching on Event
types much more reasonable.

Gateway error callbacks are also almost entirely removed in favor of
custom gateway events. A catch-all can easily be added like this:

    s.AddHandler(func(err error) {
        log.Println("gateway error:, err")
    })
2021-12-14 13:49:34 -08:00

278 lines
6.7 KiB
Go

package bot
import (
"reflect"
"sync"
"github.com/diamondburned/arikawa/v3/gateway"
"github.com/diamondburned/arikawa/v3/utils/ws"
)
var (
// eventIntents maps event pointer types to intents.
eventIntents map[reflect.Type]gateway.Intents
eventIntentsOnce sync.Once
)
func ensureEventIntents() {
eventIntentsOnce.Do(func() {
eventIntents = map[reflect.Type]gateway.Intents{}
gateway.OpUnmarshalers.Each(func(_ ws.OpCode, t ws.EventType, f ws.OpFunc) bool {
intent, ok := gateway.EventIntents[t]
if ok {
eventIntents[reflect.TypeOf(f())] = intent
}
return false
})
})
}
type command struct {
value reflect.Value // Func
event reflect.Type
isInterface bool
}
func newCommand(value reflect.Value, event reflect.Type) command {
return command{
value: value,
event: event,
isInterface: event.Kind() == reflect.Interface,
}
}
func (c *command) isEvent(t reflect.Type) bool {
return (!c.isInterface && c.event == t) || (c.isInterface && t.Implements(c.event))
}
func (c *command) call(arg0 interface{}, argv ...reflect.Value) (interface{}, error) {
return callWith(c.value, arg0, argv...)
}
// intents returns the command's intents from the event.
func (c *command) intents() gateway.Intents {
ensureEventIntents()
intents, ok := eventIntents[c.event]
if !ok {
return 0
}
return intents
}
func callWith(caller reflect.Value, arg0 interface{}, argv ...reflect.Value) (interface{}, error) {
var callargs = make([]reflect.Value, 0, 1+len(argv))
if v, ok := arg0.(reflect.Value); ok {
callargs = append(callargs, v)
} else {
callargs = append(callargs, reflect.ValueOf(arg0))
}
callargs = append(callargs, argv...)
return errorReturns(caller.Call(callargs))
}
type caller interface {
call(arg0 interface{}, argv ...reflect.Value) (interface{}, error)
}
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)
}
// MethodContext is an internal struct containing fields to make this library
// work. As such, they're all unexported. Description, however, is exported for
// editing, and may be used to generate more informative help messages.
type MethodContext struct {
command
method reflect.Method // extend
middlewares []*MiddlewareContext
Description string
// MethodName is the name of the method. This field should NOT be changed.
MethodName string
// Command is the Discord command used to call the method.
Command string // plumb if empty
// Aliases is alternative way to call command in Discord.
Aliases []string
// Hidden if true will not be shown by (*Subcommand).HelpGenerate().
Hidden bool
// Variadic is true if the function is a variadic one or if the last
// argument accepts multiple strings.
Variadic bool
Arguments []Argument
}
func parseMethod(value reflect.Value, method reflect.Method) *MethodContext {
methodT := value.Type()
numArgs := methodT.NumIn()
if numArgs == 0 {
// Doesn't meet the requirement for an event, continue.
return nil
}
// Check number of returns:
numOut := methodT.NumOut()
// Returns can either be:
// Nothing - func()
// An error - func() error
// An error and something else - func() (T, error)
if numOut > 2 {
return nil
}
// Check the last return's type if the method returns anything.
if numOut > 0 {
if i := methodT.Out(numOut - 1); i == nil || !i.Implements(typeIError) {
// Invalid, skip.
return nil
}
}
var command = MethodContext{
command: newCommand(value, methodT.In(0)),
method: method,
MethodName: method.Name,
Variadic: methodT.IsVariadic(),
}
// Only set the command name if it's a MessageCreate handler.
if command.event == typeMessageCreate {
command.Command = lowerFirstLetter(command.method.Name)
}
if numArgs > 1 {
// Event handlers that aren't MessageCreate should not have arguments.
if command.event != typeMessageCreate {
return nil
}
// If the event type is messageCreate:
command.Arguments = make([]Argument, 0, numArgs-1)
// Fill up arguments. This should work with cusP and manP
for i := 1; i < numArgs; i++ {
t := methodT.In(i)
a, err := newArgument(t, command.Variadic)
if err != nil {
panic("error parsing argument " + t.String() + ": " + err.Error())
}
command.Arguments = append(command.Arguments, *a)
// We're done if the type accepts multiple arguments.
if a.custom != nil || a.manual != nil {
command.Variadic = true // treat as variadic
break
}
}
}
return &command
}
func (cctx *MethodContext) addMiddleware(mw *MiddlewareContext) {
// Skip if mismatch type:
if !mw.command.isEvent(cctx.command.event) {
return
}
cctx.middlewares = append(cctx.middlewares, mw)
}
func (cctx *MethodContext) walkMiddlewares(ev reflect.Value) error {
for _, mw := range cctx.middlewares {
_, err := mw.call(ev)
if err != nil {
return err
}
}
return nil
}
func (cctx *MethodContext) Usage() []string {
if len(cctx.Arguments) == 0 {
return nil
}
var arguments = make([]string, len(cctx.Arguments))
for i, arg := range cctx.Arguments {
arguments[i] = arg.String
}
return arguments
}
// SetName sets the command name.
func (cctx *MethodContext) SetName(name string) {
cctx.Command = name
}
type MiddlewareContext struct {
command
}
// ParseMiddleware parses a middleware function. This function panics.
func ParseMiddleware(mw interface{}) *MiddlewareContext {
value := reflect.ValueOf(mw)
methodT := value.Type()
numArgs := methodT.NumIn()
if numArgs != 1 {
panic("Invalid argument signature for " + methodT.String())
}
// Check number of returns:
numOut := methodT.NumOut()
// Returns can either be:
// Nothing - func()
// An error - func() error
if numOut > 1 {
panic("Invalid return signature for " + methodT.String())
}
// Check the last return's type if the method returns anything.
if numOut == 1 {
if i := methodT.Out(0); i == nil || !i.Implements(typeIError) {
panic("unexpected return type (not error) for " + methodT.String())
}
}
var middleware = MiddlewareContext{
command: newCommand(value, methodT.In(0)),
}
return &middleware
}