1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-01 12:34:28 +00:00
arikawa/api/cmdroute/router.go
diamondburned 4bc415f72b
cmdroute: Add ev.Data into {Command,Autocomplete}Data
This mostly helps avoid a type assertion that could potentially be
incorrectly done.
2023-08-31 23:42:35 -07:00

348 lines
9 KiB
Go

package cmdroute
import (
"context"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/webhook"
"github.com/diamondburned/arikawa/v3/discord"
)
// Router is a router for slash commands. A zero-value Router is a valid router.
type Router struct {
nodes map[string]routeNode
mws []Middleware
stack []*Router
}
type routeNode interface {
isRouteNode()
}
type routeNodeSub struct{ *Router }
type routeNodeCommand struct {
command CommandHandler
autocomplete Autocompleter
}
type routeNodeComponent struct {
component ComponentHandler
}
func (routeNodeSub) isRouteNode() {}
func (routeNodeCommand) isRouteNode() {}
func (routeNodeComponent) isRouteNode() {}
var _ webhook.InteractionHandler = (*Router)(nil)
// NewRouter creates a new Router.
func NewRouter() *Router {
r := &Router{}
r.init()
return r
}
func (r *Router) init() {
if r.stack == nil {
r.stack = []*Router{r}
}
if r.nodes == nil {
r.nodes = make(map[string]routeNode, 4)
}
}
func (r *Router) add(name string, node routeNode) {
r.init()
_, ok := r.nodes[name]
if ok {
panic("cmdroute: node " + name + " already exists")
}
r.nodes[name] = node
}
// Use adds a middleware to the router. The middleware is applied to all
// subcommands and subrouters. Middlewares are applied in the order they are
// added, with the middlewares in the parent router being applied first.
func (r *Router) Use(mws ...Middleware) {
r.init()
r.mws = append(r.mws, mws...)
}
// Sub creates a subrouter that handles all subcommands that are under the
// parent command of the given name.
func (r *Router) Sub(name string, f func(r *Router)) {
sub := NewRouter()
sub.stack = append(append([]*Router(nil), r.stack...), sub)
f(sub)
r.add(name, routeNodeSub{sub})
}
// Add registers a slash command handler for the given command name.
func (r *Router) Add(name string, h CommandHandler) {
r.add(name, routeNodeCommand{command: h})
}
// AddFunc is a convenience function that calls Handle with a
// CommandHandlerFunc.
func (r *Router) AddFunc(name string, f CommandHandlerFunc) {
r.Add(name, f)
}
// HandleInteraction implements webhook.InteractionHandler. It only handles
// events of type CommandInteraction, otherwise nil is returned.
func (r *Router) HandleInteraction(ev *discord.InteractionEvent) *api.InteractionResponse {
switch data := ev.Data.(type) {
case *discord.CommandInteraction:
return r.HandleCommand(ev, data)
case *discord.AutocompleteInteraction:
return r.HandleAutocompletion(ev, data)
case discord.ComponentInteraction:
return r.handleComponent(ev, data)
default:
return nil
}
}
func (r *Router) callHandler(ev *discord.InteractionEvent, fn InteractionHandlerFunc) *api.InteractionResponse {
h := InteractionHandler(fn)
// Apply middlewares, parent last, first one added last. This ensures that
// when we call the handler, the middlewares are applied in the order they
// were added.
for i := len(r.stack) - 1; i >= 0; i-- {
r := r.stack[i]
for j := len(r.mws) - 1; j >= 0; j-- {
h = r.mws[j](h)
}
}
return h.HandleInteraction(context.Background(), ev)
}
// HandleCommand implements CommandHandler. It applies middlewares onto the
// handler to be executed.
//
// Deprecated: This function should not be used directly. Use HandleInteraction
// instead.
func (r *Router) HandleCommand(ev *discord.InteractionEvent, data *discord.CommandInteraction) *api.InteractionResponse {
cmdType := discord.SubcommandOptionType
if cmdIsGroup(data) {
cmdType = discord.SubcommandGroupOptionType
}
found, ok := r.findCommandHandler(ev, discord.CommandInteractionOption{
Type: cmdType,
Name: data.Name,
Options: data.Options,
})
if !ok {
return nil
}
return found.router.callCommandHandler(ev, found)
}
func cmdIsGroup(data *discord.CommandInteraction) bool {
for _, opt := range data.Options {
switch opt.Type {
case discord.SubcommandGroupOptionType, discord.SubcommandOptionType:
return true
}
}
return false
}
type handlerData struct {
router *Router
handler CommandHandler
data discord.CommandInteractionOption
}
func (r *Router) findCommandHandler(ev *discord.InteractionEvent, data discord.CommandInteractionOption) (handlerData, bool) {
node, ok := r.nodes[data.Name]
if !ok {
return handlerData{}, false
}
switch node := node.(type) {
case routeNodeSub:
if len(data.Options) != 1 || data.Type != discord.SubcommandGroupOptionType {
break
}
return node.findCommandHandler(ev, data.Options[0])
case routeNodeCommand:
if data.Type != discord.SubcommandOptionType {
break
}
return handlerData{
router: r,
handler: node.command,
data: data,
}, true
}
return handlerData{}, false
}
func (r *Router) callCommandHandler(ev *discord.InteractionEvent, found handlerData) *api.InteractionResponse {
return r.callHandler(ev,
func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
data := found.handler.HandleCommand(ctx, CommandData{
CommandInteractionOption: found.data,
Event: ev,
Data: ev.Data.(*discord.CommandInteraction),
})
if data == nil {
return nil
}
return &api.InteractionResponse{
Type: api.MessageInteractionWithSource,
Data: data,
}
},
)
}
// AddAutocompleter registers an autocompleter for the given command name.
func (r *Router) AddAutocompleter(name string, ac Autocompleter) {
r.init()
node, ok := r.nodes[name].(routeNodeCommand)
if !ok {
panic("cmdroute: cannot add autocompleter to unknown command " + name)
}
node.autocomplete = ac
r.nodes[name] = node
}
// AddAutocompleterFunc is a convenience function that calls AddAutocompleter
// with an AutocompleterFunc.
func (r *Router) AddAutocompleterFunc(name string, f AutocompleterFunc) {
r.AddAutocompleter(name, f)
}
// HandleAutocompletion handles an autocompletion event.
//
// Deprecated: This function should not be used directly. Use HandleInteraction
// instead.
func (r *Router) HandleAutocompletion(ev *discord.InteractionEvent, data *discord.AutocompleteInteraction) *api.InteractionResponse {
cmdType := discord.SubcommandOptionType
if autocompIsGroup(data) {
cmdType = discord.SubcommandGroupOptionType
}
found, ok := r.findAutocompleter(ev, discord.AutocompleteOption{
Type: cmdType,
Name: data.Name,
Options: data.Options,
})
if !ok {
return nil
}
return found.router.callAutocompletion(ev, found)
}
func autocompIsGroup(data *discord.AutocompleteInteraction) bool {
for _, opt := range data.Options {
switch opt.Type {
case discord.SubcommandGroupOptionType, discord.SubcommandOptionType:
return true
}
}
return false
}
type autocompleterData struct {
router *Router
handler Autocompleter
data discord.AutocompleteOption
}
func (r *Router) findAutocompleter(ev *discord.InteractionEvent, data discord.AutocompleteOption) (autocompleterData, bool) {
node, ok := r.nodes[data.Name]
if !ok {
return autocompleterData{}, false
}
switch node := node.(type) {
case routeNodeSub:
if len(data.Options) != 1 || data.Type != discord.SubcommandGroupOptionType {
break
}
return node.findAutocompleter(ev, data.Options[0])
case routeNodeCommand:
if node.autocomplete == nil {
break
}
if data.Type != discord.SubcommandOptionType {
break
}
return autocompleterData{
router: r,
handler: node.autocomplete,
data: data,
}, true
}
return autocompleterData{}, false
}
func (r *Router) callAutocompletion(ev *discord.InteractionEvent, found autocompleterData) *api.InteractionResponse {
return r.callHandler(ev,
func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
choices := found.handler.Autocomplete(ctx, AutocompleteData{
AutocompleteOption: found.data,
Event: ev,
Data: ev.Data.(*discord.AutocompleteInteraction),
})
if choices == nil {
return nil
}
return &api.InteractionResponse{
Type: api.AutocompleteResult,
Data: &api.InteractionResponseData{
Choices: choices,
},
}
},
)
}
// AddComponent registers a component handler for the given component ID.
func (r *Router) AddComponent(id string, f ComponentHandler) {
r.add(id, routeNodeComponent{f})
}
// AddComponentFunc is a convenience function that calls Handle with a
// ComponentHandlerFunc.
func (r *Router) AddComponentFunc(id string, f ComponentHandlerFunc) {
r.AddComponent(id, f)
}
func (r *Router) handleComponent(ev *discord.InteractionEvent, component discord.ComponentInteraction) *api.InteractionResponse {
node, ok := r.nodes[string(component.ID())].(routeNodeComponent)
if ok {
return r.callComponentHandler(ev, node.component)
}
return nil
}
func (r *Router) callComponentHandler(ev *discord.InteractionEvent, handler ComponentHandler) *api.InteractionResponse {
return r.callHandler(ev,
func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
return handler.HandleComponent(ctx, ComponentData{
Event: ev,
ComponentInteraction: ev.Data.(discord.ComponentInteraction),
})
},
)
}