feat: add watch streak miner status

This commit is contained in:
2026-04-30 17:45:39 +02:00
parent e2eb34498f
commit e30769653a
13 changed files with 1068 additions and 58 deletions
+46 -8
View File
@@ -5,14 +5,15 @@ package twitch
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"context"
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"parasocial/internal/gql"
@@ -25,7 +26,7 @@ const (
)
var (
defaultPageURL = "https://www.twitch.tv/"
defaultPageURL = "https://www.twitch.tv/"
settingsAssetPattern = regexp.MustCompile(spadeSettingsRegex)
spadeURLPattern = regexp.MustCompile(spadeURLRegex)
)
@@ -124,12 +125,14 @@ const (
// StreamerEntry is the UI-facing state for one configured streamer.
type StreamerEntry struct {
ConfigLogin string
Login string
ChannelID string
Live bool
Status StreamerStatus
Error string
ConfigLogin string
Login string
ChannelID string
Live bool
Status StreamerStatus
Error string
WatchStreak *int
WatchStreakError string
}
// ErrStreamerNotFound is returned when Twitch has no channel for the requested login.
@@ -258,6 +261,41 @@ func (s *Service) LoadChannelPointsContext(ctx context.Context, login string) (*
return context, nil
}
// WatchStreak fetches the authenticated viewer's current watch streak for one streamer.
func (s *Service) WatchStreak(ctx context.Context, login string) (*int, error) {
var data struct {
User *struct {
Channel *struct {
Self *struct {
ViewerMilestones []struct {
ID string `json:"id"`
Category string `json:"category"`
Value string `json:"value"`
} `json:"viewerMilestones"`
} `json:"self"`
} `json:"channel"`
} `json:"user"`
}
if err := s.GQL.Do(ctx, gql.WatchStreak(login), &data); err != nil {
return nil, err
}
if data.User == nil || data.User.Channel == nil || data.User.Channel.Self == nil {
return nil, fmt.Errorf("watch streak missing viewer milestones for %s", login)
}
for _, milestone := range data.User.Channel.Self.ViewerMilestones {
if milestone.Category != "WATCH_STREAK" {
continue
}
value, err := strconv.Atoi(strings.TrimSpace(milestone.Value))
if err != nil {
return nil, fmt.Errorf("parse watch streak for %s: %w", login, err)
}
return &value, nil
}
value := 0
return &value, nil
}
// ClaimCommunityPoints claims one available channel points bonus chest.
func (s *Service) ClaimCommunityPoints(ctx context.Context, channelID, claimID string) error {
var data struct {
+45
View File
@@ -146,6 +146,51 @@ func TestLoadChannelPointsContext(t *testing.T) {
}
}
func TestWatchStreakParsesMilestoneValue(t *testing.T) {
t.Parallel()
client := &fakeGQL{data: map[string]string{
"WatchStreak": `{"user":{"channel":{"self":{"viewerMilestones":[{"id":"1","category":"WATCH_STREAK","value":"12"}]}}}}`,
}}
service := &Service{GQL: client}
streak, err := service.WatchStreak(context.Background(), "streamer")
if err != nil {
t.Fatal(err)
}
if streak == nil || *streak != 12 {
t.Fatalf("streak = %#v, want 12", streak)
}
}
func TestWatchStreakReturnsZeroWhenMilestoneMissing(t *testing.T) {
t.Parallel()
client := &fakeGQL{data: map[string]string{
"WatchStreak": `{"user":{"channel":{"self":{"viewerMilestones":[{"id":"1","category":"OTHER","value":"12"}]}}}}`,
}}
service := &Service{GQL: client}
streak, err := service.WatchStreak(context.Background(), "streamer")
if err != nil {
t.Fatal(err)
}
if streak == nil || *streak != 0 {
t.Fatalf("streak = %#v, want 0", streak)
}
}
func TestWatchStreakMalformedValueErrors(t *testing.T) {
t.Parallel()
client := &fakeGQL{data: map[string]string{
"WatchStreak": `{"user":{"channel":{"self":{"viewerMilestones":[{"id":"1","category":"WATCH_STREAK","value":"many"}]}}}}`,
}}
service := &Service{GQL: client}
_, err := service.WatchStreak(context.Background(), "streamer")
if err == nil {
t.Fatal("expected error")
}
}
func TestClaimCommunityPoints(t *testing.T) {
t.Parallel()