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 }