diff --git a/twitch/eventsub.go b/twitch/eventsub.go index a67c008..29c35a8 100644 --- a/twitch/eventsub.go +++ b/twitch/eventsub.go @@ -6,8 +6,21 @@ type EventSub[Event any] struct { Subscription Subscription `json:"subscription"` // Event is the event payload. Event Event `json:"event"` + // Challenge is payload for verification challenges. + Challenge string `json:"challenge"` } // Subscription is the fields of an EventSub subscription // which are relevant to Kaiyan. -type Subscription struct{} +type Subscription struct { + // Status is the subscription status. + // For revocation messages, this holds the reason for revocation. + Status string `json:"status"` +} + +// EventSub message types. +const ( + Notification = "notification" + Verification = "webhook_callback_verification" + Revocation = "revocation" +) diff --git a/twitch/webhook.go b/twitch/webhook.go index 49f4191..5eb73fb 100644 --- a/twitch/webhook.go +++ b/twitch/webhook.go @@ -10,7 +10,10 @@ import ( "fmt" "io" "net/http" + "strconv" "sync" + + "github.com/go-json-experiment/json" ) // ErrVerificationFailed is the error returned when a webhook request does not @@ -39,14 +42,26 @@ func Receive(b, secret []byte, req *http.Request) ([]byte, error) { got := req.Header.Get("Twitch-Eventsub-Message-Signature") if !hmac.Equal(want, []byte(got)) { - fmt.Printf("req message id: %q\n", req.Header.Get("Twitch-Eventsub-Message-Id")) - fmt.Printf("req timestamp: %q\n", req.Header.Get("Twitch-Eventsub-Message-Timestamp")) - fmt.Printf("req body: %q\n", b) return nil, fmt.Errorf("%w: computed %q, header has %q", ErrVerificationFailed, want, got) } return b, nil } +// HandleChallenge responds to an EventSub verification challenge. +func HandleChallenge(w http.ResponseWriter, body []byte) error { + var ev EventSub[struct{}] + if err := json.Unmarshal(body, &ev); err != nil { + return err + } + if ev.Challenge == "" { + return errors.New("no challenge in body") + } + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Content-Length", strconv.Itoa(len(ev.Challenge))) + _, err := io.WriteString(w, ev.Challenge) + return err +} + // Secret returns a process-wide secret for webhook subscriptions. var Secret = sync.OnceValue(func() []byte { b := make([]byte, 32)