1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-27 09:12:53 +00:00
arikawa/api/cmdroute/router.go
diamondburned bc3439b8ff
cmdroute: Further deprecate Handle{Command,Autocompletion}
These functions now call HandleInteraction directly, which then calls
internal functions. For incorrect uses, this change is breaking.
2024-07-06 12:42:00 +07:00

430 lines
12 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
parent *Router // parent router, if any
groups []Router // next routers to check, if any
}
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.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.parent = r
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)
}
// Group creates a subrouter that handles certain commands within the parent
// command. This is useful for assigning middlewares to a group of commands that
// belong to the same parent command.
//
// For example, consider the following:
//
// r := cmdroute.NewRouter()
// r.Group(func(r *cmdroute.Router) {
// r.Use(cmdroute.Deferrable(client, cmdroute.DeferOpts{}))
// r.Add("foo", handleFoo)
// })
// r.Add("bar", handleBar)
//
// In this example, the middleware is only applied to the "foo" command, and not
// the "bar" command.
func (r *Router) Group(f func(r *Router)) {
f(r.With())
}
// With is similar to Group, but it returns a new router instead of calling a
// function with a new router. This is useful for chaining middlewares once,
// such as:
//
// r := cmdroute.NewRouter()
// r.With(cmdroute.Deferrable(client, cmdroute.DeferOpts{})).Add("foo", handleFoo)
func (r *Router) With(mws ...Middleware) *Router {
r.groups = append(r.groups, Router{})
sub := &r.groups[len(r.groups)-1]
sub.parent = r
sub.mws = append(sub.mws, mws...)
return sub
}
// 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
}
}
// HandleCommand implements CommandHandler. It applies middlewares onto the
// handler to be executed.
//
// Deprecated: This function should not be used directly. Use [HandleInteraction]
// instead. This function now calls HandleInteraction internally.
func (r *Router) HandleCommand(ev *discord.InteractionEvent, data *discord.CommandInteraction) *api.InteractionResponse {
return r.HandleInteraction(ev)
}
// HandleAutocompletion handles an autocompletion event.
//
// Deprecated: This function should not be used directly. Use [HandleInteraction]
// instead. This function now calls HandleInteraction internally.
func (r *Router) HandleAutocompletion(ev *discord.InteractionEvent, data *discord.AutocompleteInteraction) *api.InteractionResponse {
return r.HandleInteraction(ev)
}
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 r != nil {
for i := len(r.mws) - 1; i >= 0; i-- {
h = r.mws[i](h)
}
r = r.parent
}
return h.HandleInteraction(context.Background(), ev)
}
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
}
// findCommandHandler finds the command handler for the given command name.
// It checks the current router and its groups.
func (r *Router) findCommandHandler(ev *discord.InteractionEvent, data discord.CommandInteractionOption) (handlerData, bool) {
found, ok := r.findCommandHandlerOnce(ev, data)
if ok {
return found, true
}
for _, sub := range r.groups {
found, ok = sub.findCommandHandlerOnce(ev, data)
if ok {
return found, true
}
}
return handlerData{}, false
}
// findCommandHandlerOnce finds the command handler for the given command name.
// It only checks the current router and not its groups.
func (r *Router) findCommandHandlerOnce(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)
}
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
}
// findAutocompleter finds the autocomplete handler for the given option name.
// It checks the current router and its groups.
func (r *Router) findAutocompleter(ev *discord.InteractionEvent, data discord.AutocompleteOption) (autocompleterData, bool) {
found, ok := r.findAutocompleterOnce(ev, data)
if ok {
return found, true
}
for _, sub := range r.groups {
found, ok = sub.findAutocompleterOnce(ev, data)
if ok {
return found, true
}
}
return autocompleterData{}, false
}
// findAutocompleter finds the autocomplete handler for the given option name.
// It only checks the current router and not its groups.
func (r *Router) findAutocompleterOnce(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
}
for _, option := range data.Options {
found, ok := node.findAutocompleter(ev, option)
if ok {
return found, true
}
}
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),
})
},
)
}