discord/interaction.go

162 lines
4.4 KiB
Go

package discord
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
type InteractionContextType int
const (
InteractionContextTypeGuild InteractionContextType = iota
InteractionContextTypeBotDM
InteractionContextTypePrivateChannel
)
type InteractionType int
const (
InteractionTypePing InteractionType = iota + 1
InteractionTypeApplicationCommand
InteractionTypeMessageComponent
InteractionTypeApplicationCommandAutocomplete
InteractionTypeModalSubmit
)
type Interaction[T any] struct {
ID string `json:"id"`
ApplicationID string `json:"application_id"`
Type InteractionType `json:"type"`
Data T `json:"data"`
Guild any `json:"guild"` // TODO: Partial guilds
GuildID string `json:"guild_id"`
Channel any `json:"channel"` // TODO: Partial channels
ChannelID string `json:"channel_id"`
Member GuildMember `json:"member"` // TODO: Guild member object
User User `json:"user"`
Token string `json:"token"`
Version int `json:"version"`
Message any `json:"message"` // TODO: Message object
AppPermissions string `json:"app_permissions"`
GuildLocale string `json:"guild_locale"`
Entitlements []any `json:"entitlements"` // TODO: Entitlements
Context InteractionContextType `json:"context"`
AttachmentSizeLimit int `json:"attachment_size_limit"`
AuthorizingIntegrationOwners map[string]any `json:"authorizing_integration_owners"` // TODO: Yeah maybe not?
followupCount int
}
func (i *Interaction[T]) CallbackURL() string {
return fmt.Sprintf(
"https://discord.com/api/v10/interactions/%s/%s/callback",
i.ID, i.Token,
)
}
func (i *Interaction[T]) FollowupURL() string {
return fmt.Sprintf(
"https://discord.com/api/v10/webhooks/%s/%s", i.ApplicationID, i.Token,
)
}
func (i *Interaction[T]) Reply(token string, message ComponentsV2Message) (*http.Response, error) {
return requestWithToken(
token, http.DefaultClient, "POST", i.CallbackURL(),
InteractionResponse[ComponentsV2Message]{
Type: InteractionResponseTypeChannelMessageWithSource,
Data: message,
},
)
}
func (i *Interaction[T]) Defer(token string) (*http.Response, error) {
return requestWithToken(
token, http.DefaultClient, "POST", i.CallbackURL(),
InteractionResponse[any]{
Type: InteractionResponseTypeDeferredChannelMessageWithSource,
},
)
}
func (i *Interaction[T]) Followup(msg ComponentsV2Message) (*http.Response, error) {
if i.followupCount >= 5 {
return nil, errors.New("too many followups")
}
buf := &bytes.Buffer{}
err := json.NewEncoder(buf).Encode(msg)
if err != nil {
return nil, err
}
url := "https://discord.com/api/v10/webhooks/" + i.ApplicationID + "/" + i.Token
req, err := http.NewRequest("POST", url, buf)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
i.followupCount++
return resp, err
}
type InteractionResponseType int
const (
InteractionResponseTypePong InteractionResponseType = 1
InteractionResponseTypeChannelMessageWithSource InteractionResponseType = iota + 3
InteractionResponseTypeDeferredChannelMessageWithSource
InteractionResponseTypeDeferredUpdateMessage
InteractionResponseTypeUpdateMessage
InteractionResponseTypeApplicationCommandAutocompleteResult
InteractionResponseTypeModal
InteractionResponseTypePremiumRequired // deprecated
InteractionResponseTypeLaunchActivity InteractionResponseType = 12
)
type InteractionResponse[T any] struct {
Type InteractionResponseType `json:"type"`
Data T `json:"data,omitempty"`
}
func VerifyHTTPInteraction(publicKey ed25519.PublicKey, r *http.Request) (*Interaction[any], []byte, error) {
sigHeader := r.Header.Get("X-Signature-Ed25519")
signature, err := hex.DecodeString(sigHeader)
if err != nil {
return nil, nil, err
}
timestamp := []byte(r.Header.Get("X-Signature-Timestamp"))
bs, err := io.ReadAll(r.Body)
if err != nil {
return nil, nil, err
}
ok := ed25519.Verify(publicKey, append(timestamp, bs...), signature)
if !ok {
return nil, nil, errors.New("unable to verify ed25519 signature")
}
req := Interaction[any]{}
err = json.Unmarshal(bs, &req)
return &req, bs, err
}