162 lines
4.6 KiB
Go
162 lines
4.6 KiB
Go
// service.go defines the Twitch domain lookups the rewritten app currently needs.
|
|
// It wraps the lower-level GraphQL client with small typed methods for the viewer
|
|
// identity and configured streamer resolution used to populate the terminal UI.
|
|
package twitch
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"parasocial/internal/gql"
|
|
)
|
|
|
|
// GQLClient is the minimal GraphQL interface the Twitch service depends on.
|
|
type GQLClient interface {
|
|
Do(context.Context, gql.Request, any) error
|
|
}
|
|
|
|
// Service exposes the Twitch lookups the current app needs.
|
|
type Service struct {
|
|
GQL GQLClient
|
|
}
|
|
|
|
// Viewer is the authenticated Twitch account.
|
|
type Viewer struct {
|
|
ID string
|
|
Login string
|
|
}
|
|
|
|
// Channel is the resolved Twitch channel identity for one streamer login.
|
|
type Channel struct {
|
|
ID string
|
|
Login string
|
|
}
|
|
|
|
// StreamInfo describes whether a channel is live.
|
|
type StreamInfo struct {
|
|
Online bool
|
|
}
|
|
|
|
// PlaybackToken carries the Twitch playback token/signature pair for live HLS access.
|
|
type PlaybackToken struct {
|
|
Signature string
|
|
Value string
|
|
}
|
|
|
|
// StreamerStatus describes one row's resolution state in the UI.
|
|
type StreamerStatus string
|
|
|
|
const (
|
|
StreamerLoading StreamerStatus = "loading"
|
|
StreamerReady StreamerStatus = "ready"
|
|
StreamerError StreamerStatus = "error"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// ErrStreamerNotFound is returned when Twitch has no channel for the requested login.
|
|
var ErrStreamerNotFound = errors.New("streamer does not exist")
|
|
|
|
// LoadingStreamerEntries seeds UI state from the normalized config logins.
|
|
func LoadingStreamerEntries(logins []string) []StreamerEntry {
|
|
entries := make([]StreamerEntry, 0, len(logins))
|
|
for _, login := range logins {
|
|
entries = append(entries, StreamerEntry{
|
|
ConfigLogin: login,
|
|
Status: StreamerLoading,
|
|
})
|
|
}
|
|
return entries
|
|
}
|
|
|
|
// CurrentUser resolves the authenticated viewer through Twitch GraphQL.
|
|
func (s *Service) CurrentUser(ctx context.Context) (*Viewer, error) {
|
|
var data struct {
|
|
CurrentUser *struct {
|
|
ID string `json:"id"`
|
|
Login string `json:"login"`
|
|
} `json:"currentUser"`
|
|
}
|
|
if err := s.GQL.Do(ctx, gql.CurrentUser(), &data); err != nil {
|
|
return nil, err
|
|
}
|
|
if data.CurrentUser == nil || data.CurrentUser.ID == "" || data.CurrentUser.Login == "" {
|
|
return nil, fmt.Errorf("current user response missing id or login")
|
|
}
|
|
return &Viewer{
|
|
ID: data.CurrentUser.ID,
|
|
Login: data.CurrentUser.Login,
|
|
}, nil
|
|
}
|
|
|
|
// ResolveStreamer resolves a configured streamer login into canonical Twitch identity.
|
|
func (s *Service) ResolveStreamer(ctx context.Context, login string) (*Channel, error) {
|
|
var data struct {
|
|
User *struct {
|
|
ID string `json:"id"`
|
|
Login string `json:"login"`
|
|
} `json:"user"`
|
|
}
|
|
if err := s.GQL.Do(ctx, gql.GetIDFromLogin(login), &data); err != nil {
|
|
return nil, err
|
|
}
|
|
if data.User == nil || data.User.ID == "" {
|
|
return nil, fmt.Errorf("%w: %s", ErrStreamerNotFound, login)
|
|
}
|
|
resolvedLogin := data.User.Login
|
|
if resolvedLogin == "" {
|
|
resolvedLogin = login
|
|
}
|
|
return &Channel{ID: data.User.ID, Login: resolvedLogin}, nil
|
|
}
|
|
|
|
// StreamInfo resolves whether the given channel ID is currently live.
|
|
func (s *Service) StreamInfo(ctx context.Context, channelID string) (*StreamInfo, error) {
|
|
var data struct {
|
|
User *struct {
|
|
Stream *struct {
|
|
ID string `json:"id"`
|
|
} `json:"stream"`
|
|
} `json:"user"`
|
|
}
|
|
if err := s.GQL.Do(ctx, gql.WithIsStreamLiveQuery(channelID), &data); err != nil {
|
|
return nil, err
|
|
}
|
|
if data.User == nil {
|
|
return nil, fmt.Errorf("stream info missing user for channel %s", channelID)
|
|
}
|
|
if data.User.Stream == nil {
|
|
return &StreamInfo{Online: false}, nil
|
|
}
|
|
return &StreamInfo{Online: true}, nil
|
|
}
|
|
|
|
// PlaybackAccessToken fetches the Twitch playback token needed to access a live stream.
|
|
func (s *Service) PlaybackAccessToken(ctx context.Context, login string) (*PlaybackToken, error) {
|
|
var data struct {
|
|
StreamPlaybackAccessToken *struct {
|
|
Signature string `json:"signature"`
|
|
Value string `json:"value"`
|
|
} `json:"streamPlaybackAccessToken"`
|
|
}
|
|
if err := s.GQL.Do(ctx, gql.PlaybackAccessToken(login), &data); err != nil {
|
|
return nil, err
|
|
}
|
|
if data.StreamPlaybackAccessToken == nil || data.StreamPlaybackAccessToken.Signature == "" || data.StreamPlaybackAccessToken.Value == "" {
|
|
return nil, fmt.Errorf("playback access token missing signature or value for %s", login)
|
|
}
|
|
return &PlaybackToken{
|
|
Signature: data.StreamPlaybackAccessToken.Signature,
|
|
Value: data.StreamPlaybackAccessToken.Value,
|
|
}, nil
|
|
}
|