feat: add twitch channel points miner runtime
This commit is contained in:
+31
-11
@@ -15,6 +15,7 @@ import (
|
||||
"parasocial/internal/auth"
|
||||
"parasocial/internal/gql"
|
||||
"parasocial/internal/irc"
|
||||
"parasocial/internal/miner"
|
||||
"parasocial/internal/twitch"
|
||||
)
|
||||
|
||||
@@ -135,7 +136,10 @@ func (r *runtime) startResolve(state *auth.State, ch chan<- StreamerUpdate) {
|
||||
ch <- StreamerUpdate{Err: err, Done: true}
|
||||
return
|
||||
}
|
||||
if err := resolveStreamerEntriesWithSleep(r.ctx, service, state, r.logins, ircManager, func(update StreamerUpdate) {
|
||||
minerManager := miner.NewManager(r.ctx, service, nil)
|
||||
defer minerManager.Close()
|
||||
|
||||
if err := resolveStreamerEntriesWithSleep(r.ctx, service, state, r.logins, ircManager, minerManager, func(update StreamerUpdate) {
|
||||
ch <- update
|
||||
}, r.refreshEvery, r.sleep); err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
@@ -153,12 +157,22 @@ type streamerService interface {
|
||||
ResolveStreamer(context.Context, string) (*twitch.Channel, error)
|
||||
StreamInfo(context.Context, string) (*twitch.StreamInfo, error)
|
||||
PlaybackAccessToken(context.Context, string) (*twitch.PlaybackToken, error)
|
||||
LoadChannelPointsContext(context.Context, string) (*twitch.ChannelPointsContext, error)
|
||||
ClaimCommunityPoints(context.Context, string, string) error
|
||||
StreamMetadata(context.Context, string) (*twitch.StreamMetadata, error)
|
||||
FetchSpadeURL(context.Context, string) (string, error)
|
||||
TouchPlayback(context.Context, string, *twitch.PlaybackToken) error
|
||||
SendMinuteWatched(context.Context, string, twitch.MinuteWatchedPayload) error
|
||||
}
|
||||
|
||||
type ircSyncer interface {
|
||||
Sync(context.Context, string, string, []irc.Target)
|
||||
}
|
||||
|
||||
type minerSyncer interface {
|
||||
Sync(context.Context, *auth.State, *twitch.Viewer, []twitch.StreamerEntry)
|
||||
}
|
||||
|
||||
func newTwitchService(httpClient *http.Client, state *auth.State) (*twitch.Service, error) {
|
||||
client := &gql.Client{
|
||||
HTTPClient: httpClient,
|
||||
@@ -172,7 +186,11 @@ func newTwitchService(httpClient *http.Client, state *auth.State) (*twitch.Servi
|
||||
if err := client.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("configure graphql session: %w", err)
|
||||
}
|
||||
return &twitch.Service{GQL: client}, nil
|
||||
return &twitch.Service{
|
||||
GQL: client,
|
||||
HTTPClient: httpClient,
|
||||
Session: client.Session,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newIRCManager(ch chan<- StreamerUpdate) *irc.Manager {
|
||||
@@ -190,11 +208,11 @@ func newIRCManager(ch chan<- StreamerUpdate) *irc.Manager {
|
||||
}
|
||||
}
|
||||
|
||||
func resolveStreamerEntries(ctx context.Context, service streamerService, state *auth.State, logins []string, syncer ircSyncer, send func(StreamerUpdate)) error {
|
||||
return resolveStreamerEntriesWithSleep(ctx, service, state, logins, syncer, send, streamerRefreshInterval, sleepContext)
|
||||
func resolveStreamerEntries(ctx context.Context, service streamerService, state *auth.State, logins []string, ircSyncer ircSyncer, minerSyncer minerSyncer, send func(StreamerUpdate)) error {
|
||||
return resolveStreamerEntriesWithSleep(ctx, service, state, logins, ircSyncer, minerSyncer, send, streamerRefreshInterval, sleepContext)
|
||||
}
|
||||
|
||||
func resolveStreamerEntriesWithSleep(ctx context.Context, service streamerService, state *auth.State, logins []string, syncer ircSyncer, send func(StreamerUpdate), interval time.Duration, sleep func(context.Context, time.Duration) error) error {
|
||||
func resolveStreamerEntriesWithSleep(ctx context.Context, service streamerService, state *auth.State, logins []string, ircSyncer ircSyncer, minerSyncer minerSyncer, send func(StreamerUpdate), interval time.Duration, sleep func(context.Context, time.Duration) error) error {
|
||||
viewer, err := service.CurrentUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve current user: %w", err)
|
||||
@@ -202,7 +220,7 @@ func resolveStreamerEntriesWithSleep(ctx context.Context, service streamerServic
|
||||
send(StreamerUpdate{Viewer: viewer})
|
||||
|
||||
entries := twitch.LoadingStreamerEntries(logins)
|
||||
syncWatchedChannels(ctx, syncer, state, entries)
|
||||
syncRuntimeState(ctx, ircSyncer, minerSyncer, state, viewer, entries)
|
||||
|
||||
for {
|
||||
for index, login := range logins {
|
||||
@@ -215,7 +233,7 @@ func resolveStreamerEntriesWithSleep(ctx context.Context, service streamerServic
|
||||
Index: index,
|
||||
Entry: entry,
|
||||
})
|
||||
syncWatchedChannels(ctx, syncer, state, entries)
|
||||
syncRuntimeState(ctx, ircSyncer, minerSyncer, state, viewer, entries)
|
||||
}
|
||||
if err := sleep(ctx, interval); err != nil {
|
||||
return err
|
||||
@@ -276,11 +294,13 @@ func sleepContext(ctx context.Context, duration time.Duration) error {
|
||||
}
|
||||
}
|
||||
|
||||
func syncWatchedChannels(ctx context.Context, syncer ircSyncer, state *auth.State, entries []twitch.StreamerEntry) {
|
||||
if syncer == nil || state == nil {
|
||||
return
|
||||
func syncRuntimeState(ctx context.Context, ircSyncer ircSyncer, minerSyncer minerSyncer, state *auth.State, viewer *twitch.Viewer, entries []twitch.StreamerEntry) {
|
||||
if ircSyncer != nil && state != nil {
|
||||
ircSyncer.Sync(ctx, state.Login, state.AccessToken, watchedIRCTargets(entries))
|
||||
}
|
||||
if minerSyncer != nil {
|
||||
minerSyncer.Sync(ctx, state, viewer, entries)
|
||||
}
|
||||
syncer.Sync(ctx, state.Login, state.AccessToken, watchedIRCTargets(entries))
|
||||
}
|
||||
|
||||
func watchedIRCTargets(entries []twitch.StreamerEntry) []irc.Target {
|
||||
|
||||
@@ -59,6 +59,30 @@ func (f *fakeStreamerService) PlaybackAccessToken(_ context.Context, login strin
|
||||
return &twitch.PlaybackToken{Signature: "sig", Value: "token"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeStreamerService) LoadChannelPointsContext(context.Context, string) (*twitch.ChannelPointsContext, error) {
|
||||
return &twitch.ChannelPointsContext{Balance: 0}, nil
|
||||
}
|
||||
|
||||
func (f *fakeStreamerService) ClaimCommunityPoints(context.Context, string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeStreamerService) StreamMetadata(context.Context, string) (*twitch.StreamMetadata, error) {
|
||||
return &twitch.StreamMetadata{BroadcastID: "broadcast"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeStreamerService) FetchSpadeURL(context.Context, string) (string, error) {
|
||||
return "https://spade.test", nil
|
||||
}
|
||||
|
||||
func (f *fakeStreamerService) TouchPlayback(context.Context, string, *twitch.PlaybackToken) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeStreamerService) SendMinuteWatched(context.Context, string, twitch.MinuteWatchedPayload) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeIRCSyncer struct {
|
||||
calls [][]string
|
||||
}
|
||||
@@ -71,6 +95,21 @@ func (f *fakeIRCSyncer) Sync(_ context.Context, _, _ string, targets []irc.Targe
|
||||
f.calls = append(f.calls, logins)
|
||||
}
|
||||
|
||||
type fakeMinerSyncer struct {
|
||||
calls [][]string
|
||||
}
|
||||
|
||||
func (f *fakeMinerSyncer) Sync(_ context.Context, _ *auth.State, _ *twitch.Viewer, entries []twitch.StreamerEntry) {
|
||||
logins := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.ChannelID == "" {
|
||||
continue
|
||||
}
|
||||
logins = append(logins, entry.ChannelID)
|
||||
}
|
||||
f.calls = append(f.calls, logins)
|
||||
}
|
||||
|
||||
func TestResolveStreamerEntries(t *testing.T) {
|
||||
service := streamService(
|
||||
channel("alpha", "1", "alpha_live"),
|
||||
@@ -78,7 +117,7 @@ func TestResolveStreamerEntries(t *testing.T) {
|
||||
live("1", true),
|
||||
)
|
||||
|
||||
updates := collectUpdates(t, service, []string{"alpha", "beta"}, nil, cancelAfter(0))
|
||||
updates := collectUpdates(t, service, []string{"alpha", "beta"}, nil, nil, cancelAfter(0))
|
||||
if len(updates) != 3 {
|
||||
t.Fatalf("len(updates) = %d", len(updates))
|
||||
}
|
||||
@@ -98,7 +137,7 @@ func TestResolveStreamerEntriesPlaybackFailureStillMarksLive(t *testing.T) {
|
||||
channel("alpha", "1", "alpha_live"),
|
||||
live("1", true),
|
||||
playbackErr("alpha_live", errors.New("token lookup failed")),
|
||||
), []string{"alpha"}, nil, cancelAfter(0))
|
||||
), []string{"alpha"}, nil, nil, cancelAfter(0))
|
||||
|
||||
if entry := updates[1].Entry; entry == nil || entry.Status != twitch.StreamerReady || !entry.Live {
|
||||
t.Fatalf("entry = %#v", entry)
|
||||
@@ -109,7 +148,7 @@ func TestResolveStreamerEntriesRefreshesLiveState(t *testing.T) {
|
||||
updates := collectUpdates(t, streamService(
|
||||
channel("alpha", "1", "alpha_live"),
|
||||
streams("1", false, true),
|
||||
), []string{"alpha"}, nil, cancelAfter(1))
|
||||
), []string{"alpha"}, nil, nil, cancelAfter(1))
|
||||
|
||||
if len(updates) != 3 {
|
||||
t.Fatalf("len(updates) = %d", len(updates))
|
||||
@@ -124,6 +163,7 @@ func TestResolveStreamerEntriesRefreshesLiveState(t *testing.T) {
|
||||
|
||||
func TestResolveStreamerEntriesSyncsTopTwoLiveChannels(t *testing.T) {
|
||||
syncer := &fakeIRCSyncer{}
|
||||
miner := &fakeMinerSyncer{}
|
||||
collectUpdates(t, streamService(
|
||||
channel("alpha", "1", "alpha_live"),
|
||||
channel("beta", "2", "beta_live"),
|
||||
@@ -131,7 +171,7 @@ func TestResolveStreamerEntriesSyncsTopTwoLiveChannels(t *testing.T) {
|
||||
live("1", true),
|
||||
live("2", true),
|
||||
live("3", true),
|
||||
), []string{"alpha", "beta", "gamma"}, syncer, cancelAfter(0))
|
||||
), []string{"alpha", "beta", "gamma"}, syncer, miner, cancelAfter(0))
|
||||
|
||||
assertSyncCalls(t, syncer.calls, [][]string{
|
||||
{},
|
||||
@@ -139,16 +179,23 @@ func TestResolveStreamerEntriesSyncsTopTwoLiveChannels(t *testing.T) {
|
||||
{"alpha_live", "beta_live"},
|
||||
{"alpha_live", "beta_live"},
|
||||
})
|
||||
assertSyncCalls(t, miner.calls, [][]string{
|
||||
{},
|
||||
{"1"},
|
||||
{"1", "2"},
|
||||
{"1", "2", "3"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveStreamerEntriesRefreshUpdatesWatchedChannels(t *testing.T) {
|
||||
syncer := &fakeIRCSyncer{}
|
||||
miner := &fakeMinerSyncer{}
|
||||
collectUpdates(t, streamService(
|
||||
channel("alpha", "1", "alpha_live"),
|
||||
channel("beta", "2", "beta_live"),
|
||||
streams("1", true, false),
|
||||
streams("2", false, true),
|
||||
), []string{"alpha", "beta"}, syncer, cancelAfter(1))
|
||||
), []string{"alpha", "beta"}, syncer, miner, cancelAfter(1))
|
||||
|
||||
assertSyncCalls(t, syncer.calls, [][]string{
|
||||
{},
|
||||
@@ -205,10 +252,10 @@ func playbackErr(login string, err error) serviceOption {
|
||||
}
|
||||
}
|
||||
|
||||
func collectUpdates(t *testing.T, service *fakeStreamerService, logins []string, syncer ircSyncer, sleep func(context.Context, time.Duration) error) []StreamerUpdate {
|
||||
func collectUpdates(t *testing.T, service *fakeStreamerService, logins []string, ircSyncer ircSyncer, minerSyncer minerSyncer, sleep func(context.Context, time.Duration) error) []StreamerUpdate {
|
||||
t.Helper()
|
||||
var updates []StreamerUpdate
|
||||
err := resolveStreamerEntriesWithSleep(context.Background(), service, &auth.State{Login: "viewer", AccessToken: "token"}, logins, syncer, func(update StreamerUpdate) {
|
||||
err := resolveStreamerEntriesWithSleep(context.Background(), service, &auth.State{Login: "viewer", AccessToken: "token"}, logins, ircSyncer, minerSyncer, func(update StreamerUpdate) {
|
||||
updates = append(updates, update)
|
||||
}, 0, sleep)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
|
||||
Reference in New Issue
Block a user