Skip to content

Commit 84d3cf9

Browse files
committed
Rewards app now shows remaining channel points
Config now fetches auth token context Fix models for CommunityPointsUser and CommunityMomentsChannel Fix Moments miner
1 parent 2af1a30 commit 84d3cf9

12 files changed

Lines changed: 270 additions & 166 deletions

File tree

cmd/rewards.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"github.com/Adeithe/go-twitch"
45
tea "github.com/charmbracelet/bubbletea"
56
"github.com/spf13/cobra"
67
"log"
@@ -21,7 +22,11 @@ var rewardsCmd = &cobra.Command{
2122
}
2223

2324
s := strings.ToLower(args[0])
24-
m := tui.NewModel(s, c.AuthToken)
25+
26+
pc := twitch.PubSub()
27+
defer pc.Close()
28+
29+
m := tui.NewModel(pc, c, s)
2530

2631
p := tea.NewProgram(m, tea.WithAltScreen())
2732

internal/app/miner/moments/moments.go

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,12 @@ func MineMoments(c *pubsub.Client, streamerByIds map[string]string, authToken st
3333
return
3434
}
3535

36-
var message communitymomentschannel.Message
37-
if err := json.Unmarshal([]byte(resp.Data.Message), &message); err != nil {
38-
log.Printf("could not unmarshal response message: %s, error: %s\n", resp.Data.Message, err)
39-
return
40-
}
41-
42-
if message.Type == "active" {
43-
log.Printf("Active moment received, data: %s\n", message.Data)
44-
var data communitymomentschannel.ActiveMomentData
45-
if err := json.Unmarshal(message.Data, &data); err != nil {
46-
log.Printf("could not unmarshal response message data: %s, error: %s\n", message.Data, err)
47-
return
48-
}
49-
50-
log.Printf("Attempting to redeem moment ID: '%s'\n", data.MomentId)
51-
err := communitymomentcalloutclaim.Claim(data.MomentId, authToken)
36+
momentId := resp.Data.MomentId
37+
if len(momentId) > 0 {
38+
log.Printf("Attempting to redeem moment ID: '%s'\n", momentId)
39+
err := communitymomentcalloutclaim.Claim(momentId, authToken)
5240
if err != nil {
53-
log.Printf("could not claim moment: %s, error: %s\n", data.MomentId, err)
41+
log.Printf("could not claim moment: %s, error: %s\n", momentId, err)
5442
}
5543
}
5644
}

internal/app/rewards/tui/model.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
package tui
22

33
import (
4-
"context"
54
"fmt"
5+
"github.com/Adeithe/go-twitch/pubsub"
66
"github.com/charmbracelet/bubbles/list"
77
tea "github.com/charmbracelet/bubbletea"
88
"log"
99
"sort"
10+
"ttv-cli/internal/pkg/config"
11+
"ttv-cli/internal/pkg/twitch/gql/operation/channelpointscontext"
1012
"ttv-cli/internal/pkg/twitch/gql/query/channel"
1113
"ttv-cli/internal/pkg/twitch/pubsub/communitypointschannel"
14+
"ttv-cli/internal/pkg/twitch/pubsub/communitypointsuser"
1215
)
1316

1417
type Model struct {
1518
twitchChannel channel.Channel
16-
authToken string
19+
config config.Config
1720
list list.Model
1821
itemsById map[string]*item
1922
rewardsUpdateChannel chan communitypointschannel.Response
23+
pointsUpdateChannel chan communitypointsuser.Response
24+
pubsubClient *pubsub.Client
2025
}
2126

22-
func NewModel(streamer string, authToken string) Model {
27+
func NewModel(pubsubClient *pubsub.Client, config config.Config, streamer string) Model {
2328
c, err := channel.GetChannel(streamer)
2429
if err != nil {
2530
log.Fatalf("Failed to get channel information for '%s' - %s", streamer, err)
@@ -30,20 +35,31 @@ func NewModel(streamer string, authToken string) Model {
3035

3136
m := Model{
3237
twitchChannel: c,
33-
authToken: authToken,
38+
config: config,
3439
list: list.New(make([]list.Item, 0), list.NewDefaultDelegate(), 0, 0),
3540
itemsById: make(map[string]*item),
3641
rewardsUpdateChannel: make(chan communitypointschannel.Response),
42+
pointsUpdateChannel: make(chan communitypointsuser.Response),
3743
}
3844

39-
m.list.Title = fmt.Sprintf("%s's Rewards", m.twitchChannel.DisplayName)
45+
channelPointsContext, err := channelpointscontext.Get(c.Name, config.AuthToken)
46+
if err != nil {
47+
log.Fatalf("Could not fetch channel points context: %s", err)
48+
}
49+
50+
balance := channelPointsContext.Data.Community.Channel.Self.CommunityPoints.Balance
51+
52+
m.list.Title = fmt.Sprintf("%s's Rewards (%d points)", m.twitchChannel.DisplayName, balance)
53+
54+
m.pubsubClient = pubsubClient
55+
4056
return m
4157
}
4258

4359
func (m Model) Init() tea.Cmd {
44-
ctx := context.Background() // TODO: Close this context on app exit
45-
go m.subscribeToRewards(ctx)
46-
return tea.Batch(m.getInitialRewards, m.tick())
60+
go m.subscribeToRewards()
61+
go m.subscribeToPoints()
62+
return tea.Batch(m.getInitialRewards, m.processPointsUpdates, m.tick())
4763
}
4864

4965
func (m Model) View() string {

internal/app/rewards/tui/update.go

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
package tui
22

33
import (
4-
"context"
54
"encoding/json"
65
"fmt"
7-
"github.com/Adeithe/go-twitch"
86
"github.com/charmbracelet/bubbles/list"
97
tea "github.com/charmbracelet/bubbletea"
108
"log"
119
"time"
1210
"ttv-cli/internal/pkg/twitch/gql/operation/redeemcustomreward"
1311
"ttv-cli/internal/pkg/twitch/pubsub/communitypointschannel"
12+
"ttv-cli/internal/pkg/twitch/pubsub/communitypointsuser"
1413
)
1514

1615
type initialRewards []list.Item
1716
type updatedReward communitypointschannel.UpdatedReward
1817
type tick int
18+
type newBalance int
1919

2020
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
2121
switch msg := msg.(type) {
2222
case initialRewards:
2323
cmd := m.list.SetItems(msg)
24-
return m, tea.Batch(cmd, m.processUpdates, m.tick())
24+
return m, tea.Batch(cmd, m.processRewardUpdates, m.tick())
2525

2626
case updatedReward:
2727
// Reward has been paused or disabled, remove it from the list
@@ -35,7 +35,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3535
} else if item, ok := m.itemsById[msg.Id]; ok {
3636
item.CooldownExpiresAt = msg.CooldownExpiresAt
3737
}
38-
return m, m.processUpdates
38+
return m, m.processRewardUpdates
39+
40+
case newBalance:
41+
m.list.Title = fmt.Sprintf("%s's Rewards (%d points)", m.twitchChannel.DisplayName, msg)
42+
return m, m.processPointsUpdates
3943

4044
case tick:
4145
cmd := m.list.SetItems(m.list.Items()) // Force re-render
@@ -72,29 +76,52 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7276
return m, cmd
7377
}
7478

75-
func (m Model) subscribeToRewards(ctx context.Context) {
76-
p := twitch.PubSub()
77-
err := p.Listen("community-points-channel-v1", m.twitchChannel.Id)
79+
func (m Model) subscribeToRewards() {
80+
err := m.pubsubClient.Listen("community-points-channel-v1", m.twitchChannel.Id)
7881
if err != nil {
79-
log.Fatalln(err)
82+
log.Fatalf("Could not subscribe to community-points-channel-v1: %s\n", err)
8083
}
8184

82-
defer p.Close()
83-
84-
handleUpdate := func(_ int, _ string, data []byte) {
85-
response := communitypointschannel.Response{}
86-
if err := json.Unmarshal(data, &response); err != nil {
87-
log.Fatalln(err)
85+
subscribedTopic := "community-points-channel-v1." + m.twitchChannel.Id
86+
handleUpdate := func(_ int, topic string, data []byte) {
87+
if topic == subscribedTopic {
88+
var response communitypointschannel.Response
89+
if err := json.Unmarshal(data, &response); err != nil {
90+
log.Fatalln(err)
91+
}
92+
m.rewardsUpdateChannel <- response
8893
}
89-
m.rewardsUpdateChannel <- response
9094
}
9195

92-
p.OnShardMessage(handleUpdate)
96+
m.pubsubClient.OnShardMessage(handleUpdate)
97+
}
98+
99+
func (m Model) subscribeToPoints() {
100+
userId := m.config.TokenDetails.UserId
93101

94-
<-ctx.Done()
102+
err := m.pubsubClient.ListenWithAuth(m.config.AuthToken, "community-points-user-v1", userId)
103+
if err != nil {
104+
log.Fatalf("Could not subscribe to community-points-user-v1.%s: %s\n", m.twitchChannel.Id, err)
105+
}
106+
107+
subscribedTopic := "community-points-user-v1." + userId
108+
handleUpdate := func(_ int, topic string, data []byte) {
109+
if topic == subscribedTopic {
110+
var response communitypointsuser.Response
111+
if err := json.Unmarshal(data, &response); err != nil {
112+
log.Fatalf("Failed to unmarshal response: %s, error: %s\n", data, err)
113+
}
114+
115+
if response.Type == "points-earned" || response.Type == "points-spent" {
116+
m.pointsUpdateChannel <- response
117+
}
118+
}
119+
}
120+
121+
m.pubsubClient.OnShardMessage(handleUpdate)
95122
}
96123

97-
func (m Model) processUpdates() tea.Msg {
124+
func (m Model) processRewardUpdates() tea.Msg {
98125
for update := range m.rewardsUpdateChannel {
99126
if update.Type == "custom-reward-updated" {
100127
return updatedReward(update.Data.UpdatedReward)
@@ -103,6 +130,25 @@ func (m Model) processUpdates() tea.Msg {
103130
return nil // Unreachable
104131
}
105132

133+
func (m Model) processPointsUpdates() tea.Msg {
134+
for update := range m.pointsUpdateChannel {
135+
if update.Type == "points-spent" {
136+
var data communitypointsuser.PointsSpentData
137+
if err := json.Unmarshal(update.Data, &data); err != nil {
138+
log.Fatalf("Could not process point update: %s, error: %s\n", update, err)
139+
}
140+
return newBalance(data.Balance.Balance)
141+
} else if update.Type == "points-earned" {
142+
var data communitypointsuser.PointsEarnedData
143+
if err := json.Unmarshal(update.Data, &data); err != nil {
144+
log.Fatalf("Could not process point update: %s, error: %s\n", update, err)
145+
}
146+
return newBalance(data.Balance.Balance)
147+
}
148+
}
149+
return nil
150+
}
151+
106152
func (m Model) tick() tea.Cmd {
107153
return tea.Tick(time.Second, func(_ time.Time) tea.Msg {
108154
return tick(0)
@@ -123,7 +169,7 @@ func (m Model) redeemReward(i *item) error {
123169
input.TextInput = ":)" // FIXME
124170
}
125171

126-
_, err := redeemcustomreward.Redeem(input, m.authToken)
172+
_, err := redeemcustomreward.Redeem(input, m.config.AuthToken)
127173
if err != nil {
128174
return fmt.Errorf("could not redeem reward: %w", err)
129175
}

internal/pkg/config/config.go

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@ package config
33
import (
44
"encoding/json"
55
"fmt"
6-
"io/ioutil"
76
"os"
87
"path"
98
"ttv-cli/internal/pkg/twitch/login"
109
)
1110

11+
type TokenDetails struct {
12+
ClientId string
13+
Login string
14+
Scopes []string
15+
UserId string
16+
ExpiresIn int
17+
}
18+
19+
// Config only stores the auth token - token details retrieved when the token is validated
1220
type Config struct {
13-
AuthToken string `json:"auth_token"`
21+
AuthToken string `json:"auth_token"`
22+
TokenDetails TokenDetails
1423
}
1524

1625
func GetConfigFilePath() string {
@@ -21,8 +30,7 @@ func GetConfigFilePath() string {
2130
func createDefaultConfig() (Config, error) {
2231
emptyConfig := Config{AuthToken: ""}
2332

24-
err := emptyConfig.validateAuthToken()
25-
if err != nil {
33+
if err := emptyConfig.validateAuthToken(); err != nil {
2634
return Config{}, fmt.Errorf("createDefaultConfig: error when validating Twitch auth token: %w", err)
2735
}
2836

@@ -31,7 +39,7 @@ func createDefaultConfig() (Config, error) {
3139

3240
func CreateOrRead() (Config, error) {
3341
configFilePath := GetConfigFilePath()
34-
contents, err := ioutil.ReadFile(configFilePath)
42+
contents, err := os.ReadFile(configFilePath)
3543
if err != nil {
3644
if os.IsNotExist(err) {
3745
return createDefaultConfig()
@@ -44,31 +52,55 @@ func CreateOrRead() (Config, error) {
4452
return Config{}, fmt.Errorf("CreateOrRead: Error when unmarshalling config file: %w", err)
4553
}
4654

47-
if err = config.validateAuthToken(); err != nil {
55+
if err := config.validateAuthToken(); err != nil {
4856
return Config{}, fmt.Errorf("CreateOrRead: Error when validating Twitch auth token: %w", err)
4957
}
5058

5159
return config, nil
5260
}
5361

54-
func (c Config) validateAuthToken() error {
55-
if len(c.AuthToken) == 0 || login.Validate(c.AuthToken) != nil {
56-
fmt.Println("Auth token not found or expired, generating a new one for you...")
62+
func (c *Config) refreshAuthToken() error {
63+
authToken, err := login.GetAccessToken("", "")
64+
if err != nil {
65+
return fmt.Errorf("validateAuthToken: Error getting Twitch access token: %w", err)
66+
}
67+
68+
c.AuthToken = authToken
69+
if err := c.Save(); err != nil {
70+
return fmt.Errorf("validateAuthToken: Error saving config: %w", err)
71+
}
5772

58-
authToken, err := login.GetAccessToken("", "")
59-
if err != nil {
60-
return fmt.Errorf("validateAuthToken: Error getting Twitch access token: %w", err)
73+
return nil
74+
}
75+
76+
func (c *Config) validateAuthToken() error {
77+
if len(c.AuthToken) == 0 {
78+
fmt.Println("Auth token not found, generating a new one for you...")
79+
if err := c.refreshAuthToken(); err != nil {
80+
return fmt.Errorf("could not refresh auth token: %w", err)
6181
}
82+
}
6283

63-
c.AuthToken = authToken
64-
if err := c.Save(); err != nil {
65-
return fmt.Errorf("validateAuthToken: Error saving config: %w", err)
84+
resp, err := login.Validate(c.AuthToken)
85+
if err != nil {
86+
fmt.Println("Auth token is stale or invalid, generating a new one for you...")
87+
if err := c.refreshAuthToken(); err != nil {
88+
return fmt.Errorf("could not refresh auth token: %w", err)
6689
}
6790
}
91+
92+
c.TokenDetails = TokenDetails{
93+
ClientId: resp.ClientId,
94+
Login: resp.Login,
95+
Scopes: resp.Scopes,
96+
UserId: resp.UserId,
97+
ExpiresIn: resp.ExpiresIn,
98+
}
99+
68100
return nil
69101
}
70102

71-
func (c Config) Save() error {
103+
func (c *Config) Save() error {
72104
configFilePath := GetConfigFilePath()
73105
contents, err := json.MarshalIndent(c, "", " ")
74106
if err != nil {
@@ -80,7 +112,7 @@ func (c Config) Save() error {
80112
return fmt.Errorf("could not create config directory: %w", err)
81113
}
82114

83-
if err := ioutil.WriteFile(configFilePath, contents, 0644); err != nil {
115+
if err := os.WriteFile(configFilePath, contents, 0644); err != nil {
84116
return fmt.Errorf("could not write to config file: %w", err)
85117
}
86118

0 commit comments

Comments
 (0)