mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-11-01 04:24:19 +00:00
181dcb1bdd
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.
296 lines
7.3 KiB
Go
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
|
|
}
|