Skip to content

Commit ee80a08

Browse files
committed
Multiple field-tested bugfixes
1 parent d7325ef commit ee80a08

9 files changed

Lines changed: 273 additions & 62 deletions

File tree

pkg/streamcontrol/kick/kick.go

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"strconv"
10+
"strings"
1011
"sync"
1112
"time"
1213

@@ -136,16 +137,19 @@ func (k *Kick) keepAliveLoop(
136137
for {
137138
if k.Channel == nil { // TODO: fix non-atomicity
138139
logger.Warnf(ctx, "channel info is not set, yet")
140+
time.Sleep(time.Second)
139141
continue
140142
}
141143
client := k.GetClient()
142144
if client == nil {
143145
logger.Errorf(ctx, "client is not initialized")
146+
time.Sleep(time.Second)
144147
continue
145148
}
146149
_, err := client.GetLivestreams(k.CloseCtx, gokick.NewLivestreamListFilter().SetBroadcasterUserIDs(int(k.Channel.UserID)))
147150
if err != nil {
148151
logger.Errorf(ctx, "unable to get my stream status: %v", err)
152+
time.Sleep(time.Second)
149153
continue
150154
}
151155
select {
@@ -822,56 +826,110 @@ func (k *Kick) RaidTo(
822826
return fmt.Errorf("not implemented")
823827
}
824828

825-
func (k *Kick) Shoutout(
829+
func (k *Kick) getChanInfoViaOldClient(
826830
ctx context.Context,
827-
chanID streamcontrol.ChatUserID,
828-
) (_err error) {
829-
logger.Debugf(ctx, "Shoutout(ctx, '%s')", chanID)
830-
defer func() { logger.Debugf(ctx, "/Shoutout(ctx, '%s'): %v", chanID, _err) }()
831+
idOrLogin streamcontrol.ChatUserID,
832+
) (_ret *gokick.ChannelResponse, _err error) {
833+
logger.Debugf(ctx, "getChanInfoViaOldClient(ctx, '%s')")
834+
defer func() { logger.Debugf(ctx, "/getChanInfoViaOldClient(ctx, '%s'): %v %v", _ret, _err) }()
835+
chanInfo, err := k.ClientOBSOLETE.GetChannelV1(ctx, string(idOrLogin))
836+
if err != nil {
837+
return nil, fmt.Errorf("unable to get chan info of '%s': %w", idOrLogin, err)
838+
}
831839

832-
if err := k.prepare(ctx); err != nil {
833-
return fmt.Errorf("unable to get a prepared client: %w", err)
840+
result := &gokick.ChannelResponse{
841+
BannerPicture: chanInfo.BannerImage.URL,
842+
BroadcasterUserID: int(chanInfo.UserID),
843+
Slug: chanInfo.Slug,
844+
StreamTitle: chanInfo.Livestream.SessionTitle,
845+
}
846+
if len(chanInfo.RecentCategories) > 0 {
847+
cat := chanInfo.RecentCategories[0]
848+
result.Category = gokick.CategoryResponse{
849+
ID: int(cat.ID),
850+
Name: cat.Name,
851+
Thumbnail: cat.Category.Icon,
852+
}
834853
}
835854

836-
reply, err := k.ClientOBSOLETE.GetChannelV1(ctx, string(chanID))
855+
return result, nil
856+
}
857+
858+
func (k *Kick) getChanInfo(
859+
ctx context.Context,
860+
idOrLogin streamcontrol.ChatUserID,
861+
) (_ret *gokick.ChannelResponse, _err error) {
862+
logger.Debugf(ctx, "getChanInfo(ctx, '%s')")
863+
defer func() { logger.Debugf(ctx, "/getChanInfo(ctx, '%s'): %v %v", _ret, _err) }()
864+
865+
id, idConvErr := strconv.ParseInt(string(idOrLogin), 10, 64)
866+
867+
client := k.GetClient()
868+
if client == nil {
869+
err := fmt.Errorf("kick client is not initialized")
870+
if idConvErr != nil {
871+
logger.Errorf(ctx, "%v", err)
872+
return k.getChanInfoViaOldClient(ctx, idOrLogin)
873+
}
874+
return nil, err
875+
}
876+
877+
if idConvErr == nil {
878+
resp, err := client.GetChannels(ctx, gokick.NewChannelListFilter().SetBroadcasterUserIDs([]int{int(id)}))
879+
if err != nil {
880+
return nil, fmt.Errorf("unable to request channel info by id %d: %w", id, err)
881+
}
882+
if len(resp.Result) != 0 {
883+
return &resp.Result[0], nil
884+
}
885+
}
886+
887+
resp, err := client.GetChannels(ctx, gokick.NewChannelListFilter().SetSlug([]string{string(idOrLogin)}))
837888
if err != nil {
838-
logger.Errorf(ctx, "unable to get channel info ('%s'): %w", chanID, err)
839-
return k.sendShoutoutMessageWithoutChanInfo(ctx, chanID)
889+
logger.Errorf(ctx, "unable to request channel info by slug '%s': %v", idOrLogin, err)
890+
return k.getChanInfoViaOldClient(ctx, idOrLogin) // TODO: use an multierror to combine errors from both variants
840891
}
841-
if len(reply.PreviousLivestreams) == 0 {
842-
return k.sendShoutoutMessageWithoutChanInfo(ctx, chanID)
892+
if len(resp.Result) == 0 {
893+
return nil, fmt.Errorf("user with slug or ID '%s' is not found", idOrLogin)
843894
}
844-
return k.sendShoutoutMessage(ctx, chanID, reply.PreviousLivestreams[0])
895+
896+
return &resp.Result[0], nil
845897
}
846898

847-
func (k *Kick) sendShoutoutMessageWithoutChanInfo(
899+
func (k *Kick) Shoutout(
848900
ctx context.Context,
849-
chanID streamcontrol.ChatUserID,
901+
idOrLogin streamcontrol.ChatUserID,
850902
) (_err error) {
851-
logger.Debugf(ctx, "sendShoutoutMessageWithoutChanInfo(ctx, '%s')", chanID)
852-
defer func() { logger.Debugf(ctx, "/sendShoutoutMessageWithoutChanInfo(ctx, '%s'): %v", chanID, _err) }()
903+
logger.Debugf(ctx, "Shoutout(ctx, '%s')", idOrLogin)
904+
defer func() { logger.Debugf(ctx, "/Shoutout(ctx, '%s'): %v", idOrLogin, _err) }()
853905

854906
if err := k.prepare(ctx); err != nil {
855907
return fmt.Errorf("unable to get a prepared client: %w", err)
856908
}
857909

858-
err := k.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Take a look at their channel and click that follow button! https://www.twitch.tv/%s", chanID, chanID))
910+
chanInfo, err := k.getChanInfo(ctx, idOrLogin)
859911
if err != nil {
860-
return fmt.Errorf("unable to send the message (case #0): %w", err)
912+
return fmt.Errorf("unable to get channel info ('%s'): %w", idOrLogin, err)
861913
}
862914

863-
return nil
915+
return k.sendShoutoutMessage(ctx, *chanInfo)
864916
}
865917

866918
func (k *Kick) sendShoutoutMessage(
867919
ctx context.Context,
868-
chanID streamcontrol.ChatUserID,
869-
stream kickcom.LivestreamV1,
920+
chanInfo gokick.ChannelResponse,
870921
) (_err error) {
871-
logger.Debugf(ctx, "sendShoutoutMessage(ctx, '%s')", chanID)
872-
defer func() { logger.Debugf(ctx, "/sendShoutoutMessage(ctx, '%s'): %v", chanID, _err) }()
922+
logger.Debugf(ctx, "sendShoutoutMessage(ctx, '%s')", spew.Sdump(chanInfo))
923+
defer func() { logger.Debugf(ctx, "/sendShoutoutMessage(ctx, '%s'): %v", spew.Sdump(chanInfo), _err) }()
924+
925+
var message []string
926+
message = append(message, fmt.Sprintf("Shoutout to %s!", chanInfo.Slug))
927+
if chanInfo.StreamTitle != "" {
928+
message = append(message, fmt.Sprintf("Their latest stream: '%s'.", chanInfo.StreamTitle))
929+
}
930+
message = append(message, fmt.Sprintf("Take a look at their channel and click that follow button! https://kick.com/%s", chanInfo.Slug))
873931

874-
err := k.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Their last stream: '%s'. Take a look at their channel and click that follow button! https://kick.com/%s", chanID, stream.SessionTitle, chanID))
932+
err := k.SendChatMessage(ctx, strings.Join(message, " "))
875933
if err != nil {
876934
return fmt.Errorf("unable to send the message (case #1): %w", err)
877935
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"log"
8+
"os"
9+
10+
"github.com/davecgh/go-spew/spew"
11+
"github.com/facebookincubator/go-belt"
12+
"github.com/facebookincubator/go-belt/tool/logger"
13+
"github.com/facebookincubator/go-belt/tool/logger/implementation/zap"
14+
"github.com/xaionaro-go/observability"
15+
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
16+
)
17+
18+
func assertNoError(err error) {
19+
if err == nil {
20+
return
21+
}
22+
log.Panic(err)
23+
}
24+
25+
func main() {
26+
l := zap.Default().WithLevel(logger.LevelTrace)
27+
ctx := context.Background()
28+
ctx = logger.CtxWithLogger(ctx, l)
29+
ctx = observability.OnInsecureDebug(ctx)
30+
logger.Default = func() logger.Logger {
31+
return l
32+
}
33+
defer belt.Flush(ctx)
34+
oldUsage := flag.Usage
35+
flag.Usage = func() {
36+
fmt.Fprintf(os.Stderr, "syntax: chatlistener [options] <channel_id>\n")
37+
oldUsage()
38+
}
39+
channelID := flag.String("channel-id", "", "")
40+
41+
clientID := flag.String("client-id", "", "")
42+
clientSecret := flag.String("client-secret", "", "")
43+
flag.Parse()
44+
if flag.NArg() != 1 {
45+
flag.Usage()
46+
os.Exit(1)
47+
}
48+
user := flag.Arg(0)
49+
50+
cfg := twitch.Config{
51+
Enable: new(bool),
52+
Config: twitch.PlatformSpecificConfig{
53+
Channel: *channelID,
54+
ClientID: *clientID,
55+
GetOAuthListenPorts: func() []uint16 {
56+
return []uint16{8092}
57+
},
58+
},
59+
}
60+
cfg.Config.ClientSecret.Set(*clientSecret)
61+
c, err := twitch.New(ctx, cfg, func(c twitch.Config) error {
62+
return nil
63+
})
64+
if err != nil {
65+
panic(err)
66+
}
67+
68+
userInfo, err := c.GetUser(user)
69+
if err != nil {
70+
panic(err)
71+
}
72+
73+
spew.Dump(userInfo)
74+
}

pkg/streamcontrol/twitch/twitch.go

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"unicode"
1010
"unicode/utf8"
1111

12+
"github.com/davecgh/go-spew/spew"
1213
"github.com/facebookincubator/go-belt"
1314
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
1415
"github.com/facebookincubator/go-belt/tool/logger"
@@ -894,7 +895,10 @@ func (t *Twitch) IsCapable(
894895
func (t *Twitch) IsChannelStreaming(
895896
ctx context.Context,
896897
chanID streamcontrol.ChatUserID,
897-
) (bool, error) {
898+
) (_ret bool, _err error) {
899+
logger.Debugf(ctx, "IsChannelStreaming")
900+
defer func() { logger.Debugf(ctx, "/IsChannelStreaming: %v %v", _ret, _err) }()
901+
898902
reply, err := t.client.GetStreams(&helix.StreamsParams{
899903
UserIDs: []string{string(chanID)},
900904
})
@@ -912,15 +916,19 @@ func (t *Twitch) IsChannelStreaming(
912916

913917
func (t *Twitch) RaidTo(
914918
ctx context.Context,
915-
chanID streamcontrol.ChatUserID,
919+
idOrLogin streamcontrol.ChatUserID,
916920
) (_err error) {
917-
logger.Debugf(ctx, "RaidTo(ctx, '%s')", chanID)
918-
defer func() { logger.Debugf(ctx, "/RaidTo(ctx, '%s'): %v", chanID, _err) }()
921+
logger.Debugf(ctx, "RaidTo(ctx, '%s')", idOrLogin)
922+
defer func() { logger.Debugf(ctx, "/RaidTo(ctx, '%s'): %v", idOrLogin, _err) }()
923+
user, err := t.GetUser(string(idOrLogin))
924+
if err != nil {
925+
return fmt.Errorf("unable to get user '%s': %w", idOrLogin, err)
926+
}
919927
params := &helix.StartRaidParams{
920928
FromBroadcasterID: t.broadcasterID,
921-
ToBroadcasterID: string(chanID),
929+
ToBroadcasterID: string(user.ID),
922930
}
923-
logger.Debugf(ctx, "RaidTo(ctx, '%s'): %#+v", chanID, params)
931+
logger.Debugf(ctx, "RaidTo(ctx, '%s'): %#+v", idOrLogin, params)
924932
resp, err := t.client.StartRaid(params)
925933
if err != nil {
926934
return fmt.Errorf("unable to raid %#+v: %v", params, err)
@@ -929,43 +937,71 @@ func (t *Twitch) RaidTo(
929937
return nil
930938
}
931939

940+
func (t *Twitch) GetUser(idOrLogin string) (*helix.User, error) {
941+
users, err := t.client.GetUsers(&helix.UsersParams{
942+
IDs: []string{string(idOrLogin)},
943+
})
944+
if err != nil {
945+
return nil, fmt.Errorf("unable to get user info for userID '%s': %w", idOrLogin, err)
946+
}
947+
if len(users.Data.Users) == 0 {
948+
users, err = t.client.GetUsers(&helix.UsersParams{
949+
Logins: []string{string(idOrLogin)},
950+
})
951+
if err != nil {
952+
return nil, fmt.Errorf("unable to get user info for login '%s': %w", idOrLogin, err)
953+
}
954+
}
955+
if len(users.Data.Users) == 0 {
956+
return nil, fmt.Errorf("user with ID-or-login '%s' not found", idOrLogin)
957+
}
958+
return &users.Data.Users[0], nil
959+
}
960+
932961
func (t *Twitch) Shoutout(
933962
ctx context.Context,
934-
chanID streamcontrol.ChatUserID,
963+
userIDOrLogin streamcontrol.ChatUserID,
935964
) (_err error) {
936-
logger.Debugf(ctx, "Shoutout(ctx, '%s')", chanID)
937-
defer func() { logger.Debugf(ctx, "/Shoutout(ctx, '%s'): %v", chanID, _err) }()
965+
logger.Debugf(ctx, "Shoutout(ctx, '%s')", userIDOrLogin)
966+
defer func() { logger.Debugf(ctx, "/Shoutout(ctx, '%s'): %v", userIDOrLogin, _err) }()
938967
params := &helix.SendShoutoutParams{
939968
FromBroadcasterID: t.broadcasterID,
940-
ToBroadcasterID: string(chanID),
969+
ToBroadcasterID: string(userIDOrLogin),
941970
ModeratorID: t.broadcasterID,
942971
}
943-
logger.Debugf(ctx, "Shoutout(ctx, '%s'): %#+v", chanID, params)
972+
logger.Debugf(ctx, "Shoutout(ctx, '%s'): %#+v", userIDOrLogin, params)
944973
_, err := t.client.SendShoutout(params)
945974
if err != nil {
946975
return fmt.Errorf("unable to send the shoutout (%#+v): %w", params, err)
947976
}
948977

978+
user, err := t.GetUser(string(userIDOrLogin))
979+
if err != nil {
980+
return fmt.Errorf("unable to get user '%s': %w", userIDOrLogin, err)
981+
}
949982
reply, err := t.client.GetStreams(&helix.StreamsParams{
950-
UserIDs: []string{string(chanID)},
983+
UserIDs: []string{string(user.ID)},
951984
})
952985
if err != nil {
953-
logger.Errorf(ctx, "unable to get channel info ('%s'): %w", chanID, err)
954-
return t.sendShoutoutMessageWithoutChanInfo(ctx, chanID)
986+
logger.Errorf(ctx, "unable to get streams info (userID: %v): %w", user.ID, err)
987+
return t.sendShoutoutMessageWithoutChanInfo(ctx, *user)
955988
}
956989
if len(reply.Data.Streams) == 0 {
957-
return t.sendShoutoutMessageWithoutChanInfo(ctx, chanID)
990+
return t.sendShoutoutMessageWithoutChanInfo(ctx, *user)
958991
}
959-
return t.sendShoutoutMessage(ctx, chanID, reply.Data.Streams[0])
992+
return t.sendShoutoutMessage(ctx, *user, reply.Data.Streams[0])
960993
}
961994

962995
func (t *Twitch) sendShoutoutMessageWithoutChanInfo(
963996
ctx context.Context,
964-
chanID streamcontrol.ChatUserID,
997+
user helix.User,
965998
) (_err error) {
966-
logger.Debugf(ctx, "sendShoutoutMessageWithoutChanInfo(ctx, '%s')", chanID)
967-
defer func() { logger.Debugf(ctx, "/sendShoutoutMessageWithoutChanInfo(ctx, '%s'): %v", chanID, _err) }()
968-
err := t.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Take a look at their channel and click that follow button! https://www.twitch.tv/%s", chanID, chanID))
999+
logger.Debugf(ctx, "sendShoutoutMessageWithoutChanInfo(ctx, '%s')", spew.Sdump(user))
1000+
defer func() {
1001+
logger.Debugf(ctx, "/sendShoutoutMessageWithoutChanInfo(ctx, '%s'): %v", spew.Sdump(user), _err)
1002+
}()
1003+
yearsExists := float64(int(time.Since(user.CreatedAt.Time).Hours()/24/364*10)) / 10
1004+
err := t.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! A great creator (%.1f years on Twitch)! Their self-description: '%s'. Take a look at their channel and click that follow button! https://www.twitch.tv/%s", user.DisplayName, yearsExists, user.Description, user.Login))
9691005
if err != nil {
9701006
return fmt.Errorf("unable to send the message (case #0): %w", err)
9711007
}
@@ -974,12 +1010,13 @@ func (t *Twitch) sendShoutoutMessageWithoutChanInfo(
9741010

9751011
func (t *Twitch) sendShoutoutMessage(
9761012
ctx context.Context,
977-
chanID streamcontrol.ChatUserID,
1013+
user helix.User,
9781014
stream helix.Stream,
9791015
) (_err error) {
980-
logger.Debugf(ctx, "sendShoutoutMessage(ctx, '%s')", chanID)
981-
defer func() { logger.Debugf(ctx, "/sendShoutoutMessage(ctx, '%s'): %v", chanID, _err) }()
982-
err := t.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Their last stream: '%s'. Take a look at their channel and click that follow button! https://www.twitch.tv/%s", chanID, stream.Title, chanID))
1016+
logger.Debugf(ctx, "sendShoutoutMessage(ctx, '%s')", spew.Sdump(user))
1017+
defer func() { logger.Debugf(ctx, "/sendShoutoutMessage(ctx, '%s'): %v", spew.Sdump(user), _err) }()
1018+
yearsExists := float64(int(time.Since(user.CreatedAt.Time).Hours()/24/364*10)) / 10
1019+
err := t.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! A great creator (%.1f years on Twitch)! Their last stream: '%s'. Their self-description: '%s'. Take a look at their channel and click that follow button! https://www.twitch.tv/%s", user.DisplayName, yearsExists, stream.Title, user.Description, user.Login))
9831020
if err != nil {
9841021
return fmt.Errorf("unable to send the message (case #1): %w", err)
9851022
}

0 commit comments

Comments
 (0)