mirror of
https://github.com/Luzifer/twitch-manager.git
synced 2024-12-21 04:11:17 +00:00
205 lines
5.5 KiB
Go
205 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/Luzifer/go_helpers/v2/str"
|
|
)
|
|
|
|
const twitchRequestTimeout = 2 * time.Second
|
|
|
|
func handleWebHookPush(w http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
vars = mux.Vars(r)
|
|
hookType = vars["type"]
|
|
|
|
logger = log.WithField("type", hookType)
|
|
)
|
|
|
|
// When asked for a confirmation, just confirm it
|
|
if challengeToken := r.URL.Query().Get("hub.challenge"); challengeToken != "" {
|
|
logger.Debug("Confirming webhook subscription")
|
|
w.Write([]byte(challengeToken))
|
|
return
|
|
}
|
|
|
|
// We're getting a reason for a denied subscription
|
|
if reason := r.URL.Query().Get("hub.reason"); reason != "" {
|
|
logger.WithField("reason", reason).Error("Webhook subscription was denied")
|
|
return
|
|
}
|
|
|
|
var (
|
|
body = new(bytes.Buffer)
|
|
signature = r.Header.Get("X-Hub-Signature")
|
|
)
|
|
|
|
if _, err := io.Copy(body, r.Body); err != nil {
|
|
logger.WithError(err).Error("Unable to read hook body")
|
|
return
|
|
}
|
|
|
|
mac := hmac.New(sha256.New, []byte(cfg.WebHookSecret))
|
|
mac.Write(body.Bytes())
|
|
if cSig := fmt.Sprintf("sha256=%x", mac.Sum(nil)); cSig != signature {
|
|
log.Errorf("Got message signature %s, expected %s", signature, cSig)
|
|
http.Error(w, "Signature verification failed", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
switch hookType {
|
|
case "donation":
|
|
var payload struct {
|
|
Name string `json:"name"`
|
|
Amount float64 `json:"amount"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
if err := json.NewDecoder(body).Decode(&payload); err != nil {
|
|
logger.WithError(err).Error("Unable to decode payload")
|
|
return
|
|
}
|
|
|
|
fields := map[string]interface{}{
|
|
"name": payload.Name,
|
|
"amount": payload.Amount,
|
|
"message": payload.Message,
|
|
}
|
|
|
|
if err := subscriptions.SendAllSockets(msgTypeDonation, fields, false, true); err != nil {
|
|
log.WithError(err).Error("Unable to send update to all sockets")
|
|
}
|
|
|
|
store.WithModLock(func() error {
|
|
store.Donations.LastAmount = payload.Amount
|
|
store.Donations.LastDonator = &payload.Name
|
|
|
|
return nil
|
|
})
|
|
|
|
case "follow":
|
|
var payload struct {
|
|
Data []struct {
|
|
FromName string `json:"from_name"`
|
|
FollowedAt time.Time `json:"followed_at"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
if err := json.NewDecoder(body).Decode(&payload); err != nil {
|
|
logger.WithError(err).Error("Unable to decode payload")
|
|
return
|
|
}
|
|
|
|
sort.Slice(payload.Data, func(i, j int) bool { return payload.Data[i].FollowedAt.Before(payload.Data[j].FollowedAt) })
|
|
for _, f := range payload.Data {
|
|
var isKnown bool
|
|
|
|
store.WithModRLock(func() error {
|
|
isKnown = str.StringInSlice(f.FromName, store.Followers.Seen)
|
|
return nil
|
|
})
|
|
|
|
if isKnown {
|
|
logger.WithField("name", f.FromName).Debug("New follower already known, skipping")
|
|
continue
|
|
}
|
|
|
|
fields := map[string]interface{}{
|
|
"from": f.FromName,
|
|
"followed_at": f.FollowedAt,
|
|
}
|
|
|
|
if err := subscriptions.SendAllSockets(msgTypeFollow, fields, false, true); err != nil {
|
|
log.WithError(err).Error("Unable to send update to all sockets")
|
|
}
|
|
|
|
logger.WithField("name", f.FromName).Info("New follower announced")
|
|
store.WithModLock(func() error {
|
|
store.Followers.Last = &f.FromName
|
|
store.Followers.Count++
|
|
store.Followers.Seen = append([]string{f.FromName}, store.Followers.Seen...)
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
default:
|
|
log.WithField("type", hookType).Warn("Received unexpected webhook request")
|
|
return
|
|
}
|
|
|
|
if err := store.Save(cfg.StoreFile); err != nil {
|
|
logger.WithError(err).Error("Unable to update persistent store")
|
|
}
|
|
|
|
if err := store.WithModRLock(func() error { return subscriptions.SendAllSockets(msgTypeStore, store, false, false) }); err != nil {
|
|
logger.WithError(err).Error("Unable to send update to all sockets")
|
|
}
|
|
}
|
|
|
|
func registerWebHooks() error {
|
|
hookURL := func(hookType string) string {
|
|
return strings.Join([]string{
|
|
strings.TrimRight(cfg.BaseURL, "/"),
|
|
"api", "webhook",
|
|
hookType,
|
|
}, "/")
|
|
}
|
|
|
|
for uri, topic := range map[string]string{
|
|
hookURL("follow"): fmt.Sprintf("https://api.twitch.tv/helix/users/follows?first=1&to_id=%s", cfg.TwitchID),
|
|
} {
|
|
ctx, cancel := context.WithTimeout(context.Background(), twitchRequestTimeout)
|
|
defer cancel()
|
|
|
|
buf := new(bytes.Buffer)
|
|
if err := json.NewEncoder(buf).Encode(map[string]interface{}{
|
|
"hub.callback": uri,
|
|
"hub.mode": "subscribe",
|
|
"hub.topic": topic,
|
|
"hub.lease_seconds": int64((cfg.WebHookTimeout + twitchRequestTimeout) / time.Second),
|
|
"hub.secret": cfg.WebHookSecret,
|
|
}); err != nil {
|
|
return errors.Wrap(err, "assemble subscribe payload")
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.twitch.tv/helix/webhooks/hub", buf)
|
|
if err != nil {
|
|
return errors.Wrap(err, "assemble subscribe request")
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Client-Id", cfg.TwitchClient)
|
|
req.Header.Set("Authorization", "Bearer "+cfg.TwitchToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return errors.Wrap(err, "requesting subscribe")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unexpected status %d, unable to read body", resp.StatusCode)
|
|
}
|
|
return errors.Errorf("unexpected status %d: %s", resp.StatusCode, body)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|