1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-16 03:44:26 +00:00
arikawa/api/cmdroute/middlewares.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

146 lines
4 KiB
Go

package cmdroute
import (
"context"
"time"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
)
type ctxKey uint8
const (
_ ctxKey = iota
ctxCtx
deferTicketCtx
)
// UseContext returns a middleware that override the handler context to the
// given context. This middleware should only be used once in the parent-most
// router.
func UseContext(ctx context.Context) Middleware {
return func(next InteractionHandler) InteractionHandler {
return InteractionHandlerFunc(func(_ context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
return next.HandleInteraction(ctx, ev)
})
}
}
// FollowUpSender is a type that can send follow-up messages. Usually, anything
// that extends *api.Client can be used as a FollowUpSender.
type FollowUpSender interface {
FollowUpInteraction(appID discord.AppID, token string, data api.InteractionResponseData) (*discord.Message, error)
}
// DeferOpts is the options for Deferrable().
type DeferOpts struct {
// Timeout is the timeout for the handler to return a response. If the
// handler does not return within this timeout, then it is deferred.
//
// Defaults to 1.5 seconds.
Timeout time.Duration
// Flags is the flags to set on the response.
Flags discord.MessageFlags
// Error is called when a follow-up message fails to send. If nil, it does
// nothing.
Error func(err error)
// Done is called when the handler is done. If nil, it does nothing.
Done func(*discord.Message)
}
// Deferrable marks a router as deferrable, meaning if the handler does not
// return a response within the deadline, the response will be automatically
// deferred.
func Deferrable(client FollowUpSender, opts DeferOpts) Middleware {
if opts.Timeout == 0 {
opts.Timeout = 1*time.Second + 500*time.Millisecond
}
return func(next InteractionHandler) InteractionHandler {
return InteractionHandlerFunc(func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
timeout, cancel := context.WithTimeout(ctx, opts.Timeout)
defer cancel()
respCh := make(chan *api.InteractionResponse, 1)
go func() {
ctx := context.WithValue(ctx, deferTicketCtx, DeferTicket{
ctx: timeout,
deferFn: cancel,
})
resp := next.HandleInteraction(ctx, ev)
if resp != nil && opts.Flags > 0 {
if resp.Data != nil {
resp.Data.Flags = opts.Flags
} else {
resp.Data = &api.InteractionResponseData{
Flags: opts.Flags,
}
}
}
respCh <- resp
}()
select {
case resp := <-respCh:
return resp
case <-timeout.Done():
go func() {
resp := <-respCh
if resp == nil || resp.Data == nil {
return
}
m, err := client.FollowUpInteraction(ev.AppID, ev.Token, *resp.Data)
if err != nil && opts.Error != nil {
opts.Error(err)
}
if m != nil && opts.Done != nil {
opts.Done(m)
}
}()
return &api.InteractionResponse{
Type: api.DeferredMessageInteractionWithSource,
Data: &api.InteractionResponseData{
Flags: opts.Flags,
},
}
}
})
}
}
// DeferTicket is a ticket that can be used to defer a slash command. It can be
// used to manually send a response later.
type DeferTicket struct {
ctx context.Context
deferFn context.CancelFunc
}
// DeferTicketFromContext returns the DeferTicket from the context. If no ticket
// is found, it returns a zero-value ticket.
func DeferTicketFromContext(ctx context.Context) DeferTicket {
ticket, _ := ctx.Value(deferTicketCtx).(DeferTicket)
return ticket
}
// IsDeferred returns true if the handler has been deferred.
func (t DeferTicket) IsDeferred() bool {
return t.Context().Err() != nil
}
// Context returns the context that is done when the handler is deferred. If
// DeferTicket is zero-value, it returns the background context.
func (t DeferTicket) Context() context.Context {
if t.ctx == nil {
return context.Background()
}
return t.ctx
}
// Defer defers the response. If DeferTicket is zero-value, it does nothing.
func (t DeferTicket) Defer() {
t.deferFn()
}