162 lines
4.4 KiB
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
|
|
}
|