1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-01 04:24:19 +00:00
arikawa/api/cmdroute/router.go
diamondburned 181dcb1bdd
api: Introduce api/cmdroute
This commit introduces a slash commands and autocompletion router. It
abstracts the switch-cases that the user has to do in each
InteractionEvent handler away.

The router is largely inspired by go-chi's design. Refer to the tests
for examples.
2022-10-13 23:01:29 -07:00

296 lines
7.3 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 struct {
sub *Router
cmd CommandHandler
com Autocompleter
}
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)
}
}
// 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)) {
r.init()
node, ok := r.nodes[name]
if ok && node.sub == nil {
panic("cmdroute: command " + name + " already exists")
}
sub := NewRouter()
sub.stack = append(append([]*Router(nil), r.stack...), sub)
f(sub)
r.nodes[name] = routeNode{sub: sub}
}
// Add registers a slash command handler for the given command name.
func (r *Router) Add(name string, h CommandHandler) {
r.init()
node, ok := r.nodes[name]
if ok {
panic("cmdroute: command " + name + " already exists")
}
node.cmd = h
r.nodes[name] = node
}
// 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)
default:
return nil
}
}
func (r *Router) handleInteraction(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.
func (r *Router) HandleCommand(ev *discord.InteractionEvent, data *discord.CommandInteraction) *api.InteractionResponse {
cmdType := discord.SubcommandOptionType
if cmdIsGroup(data) {
cmdType = discord.SubcommandGroupOptionType
}
found, ok := r.findHandler(ev, discord.CommandInteractionOption{
Type: cmdType,
Name: data.Name,
Options: data.Options,
})
if !ok {
return nil
}
return found.router.handleCommand(ev, found)
}
func (r *Router) handleCommand(ev *discord.InteractionEvent, found handlerData) *api.InteractionResponse {
return r.handleInteraction(ev,
func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
data := found.handler.HandleCommand(ctx, CommandData{
CommandInteractionOption: found.data,
Event: ev,
})
if data == nil {
return nil
}
return &api.InteractionResponse{
Type: api.MessageInteractionWithSource,
Data: data,
}
},
)
}
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) findHandler(ev *discord.InteractionEvent, data discord.CommandInteractionOption) (handlerData, bool) {
node, ok := r.nodes[data.Name]
if !ok {
return handlerData{}, false
}
switch {
case node.sub != nil:
if len(data.Options) != 1 || data.Type != discord.SubcommandGroupOptionType {
break
}
return node.sub.findHandler(ev, data.Options[0])
case node.cmd != nil:
if data.Type != discord.SubcommandOptionType {
break
}
return handlerData{
router: r,
handler: node.cmd,
data: data,
}, true
}
return handlerData{}, false
}
// AddAutocompleter registers an autocompleter for the given command name.
func (r *Router) AddAutocompleter(name string, ac Autocompleter) {
r.init()
node, ok := r.nodes[name]
if !ok || node.cmd == nil {
panic("cmdroute: command " + name + " does not exist or is not a (sub)command")
}
node.com = 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.
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.handleAutocompletion(ev, found)
}
func (r *Router) handleAutocompletion(ev *discord.InteractionEvent, found autocompleterData) *api.InteractionResponse {
return r.handleInteraction(ev,
func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
choices := found.handler.Autocomplete(ctx, AutocompleteData{
AutocompleteOption: found.data,
Event: ev,
})
if choices == nil {
return nil
}
return &api.InteractionResponse{
Type: api.AutocompleteResult,
Data: &api.InteractionResponseData{
Choices: choices,
},
}
},
)
}
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 {
case node.sub != nil:
if len(data.Options) != 1 || data.Type != discord.SubcommandGroupOptionType {
break
}
return node.sub.findAutocompleter(ev, data.Options[0])
case node.com != nil:
if data.Type != discord.SubcommandOptionType {
break
}
return autocompleterData{
router: r,
handler: node.com,
data: data,
}, true
}
return autocompleterData{}, false
}