From ebc141c05b725a943b8d176e575738fce96cac84 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sun, 12 Apr 2020 07:10:35 +0200 Subject: [PATCH] TwitchBee now emits events for new followers --- bees/twitchbee/twitchbee.go | 106 +++++++++++++++++++++++++---- bees/twitchbee/twitchbeefactory.go | 18 +++++ go.mod | 1 + go.sum | 3 +- 4 files changed, 112 insertions(+), 16 deletions(-) diff --git a/bees/twitchbee/twitchbee.go b/bees/twitchbee/twitchbee.go index c0f4bcc2..528a43e6 100644 --- a/bees/twitchbee/twitchbee.go +++ b/bees/twitchbee/twitchbee.go @@ -26,6 +26,7 @@ import ( "time" twitch "github.com/gempir/go-twitch-irc/v2" + "github.com/nicklaw5/helix" "github.com/muesli/beehive/bees" ) @@ -38,11 +39,14 @@ type TwitchBee struct { connectedState chan bool // setup Twitch client: - client *twitch.Client + chat *twitch.Client + client *helix.Client channels []string username string password string + + clientId string } // Action triggers the action passed to it. @@ -65,7 +69,7 @@ func (mod *TwitchBee) Action(action bees.Action) []bees.Placeholder { if recv == "*" { // special: send to all joined channels for _, to := range mod.channels { - mod.client.Say(to, text) + mod.chat.Say(to, text) } } else { // needs stripping hostname when sending to user!host @@ -73,7 +77,7 @@ func (mod *TwitchBee) Action(action bees.Action) []bees.Placeholder { recv = recv[0:strings.Index(recv, "!")] } - mod.client.Say(recv, text) + mod.chat.Say(recv, text) } } @@ -99,20 +103,20 @@ func (mod *TwitchBee) Action(action bees.Action) []bees.Placeholder { func (mod *TwitchBee) rejoin() { for _, channel := range mod.channels { - mod.client.Join(channel) + mod.chat.Join(channel) } } func (mod *TwitchBee) join(channel string) { channel = strings.TrimSpace(channel) - mod.client.Join(channel) + mod.chat.Join(channel) mod.channels = append(mod.channels, channel) } func (mod *TwitchBee) part(channel string) { channel = strings.TrimSpace(channel) - mod.client.Depart(channel) + mod.chat.Depart(channel) for k, v := range mod.channels { if v == channel { @@ -122,19 +126,80 @@ func (mod *TwitchBee) part(channel string) { } } +func (mod *TwitchBee) monitorFollows(eventChan chan bees.Event) { + var twitchId string + { + resp, err := mod.client.GetUsers(&helix.UsersParams{ + Logins: []string{mod.username}, + }) + if err != nil || len(resp.Data.Users) != 1 { + mod.LogErrorf("Failed retrieving user info from Twitch API: %v", err) + } + twitchId = resp.Data.Users[0].ID + } + + follows := make(map[string]helix.UserFollow) + var seeded bool + for { + var pagination string + for { + resp, err := mod.client.GetUsersFollows(&helix.UsersFollowsParams{ + After: pagination, + First: 40, + ToID: twitchId, + }) + if err != nil { + mod.LogErrorf("Failed retrieving follows from Twitch API: %v", err) + break + } + pagination = resp.Data.Pagination.Cursor + + for _, f := range resp.Data.Follows { + if _, ok := follows[f.FromID]; !ok { + follows[f.FromID] = f + + if seeded { + ev := bees.Event{ + Bee: mod.Name(), + Name: "follow", + Options: []bees.Placeholder{ + { + Name: "user", + Type: "string", + Value: f.FromName, + }, + }, + } + eventChan <- ev + } + } + } + + if len(resp.Data.Follows) == 0 || pagination == "" { + // end of paginated list + seeded = true + break + } + } + + // poll once a minute + time.Sleep(30 * time.Second) + } +} + // Run executes the Bee's event loop. func (mod *TwitchBee) Run(eventChan chan bees.Event) { // channel signaling Twitch connection status mod.connectedState = make(chan bool) - // setup Twitch client: - mod.client = twitch.NewClient(mod.username, mod.password) + // setup Twitch c: + mod.chat = twitch.NewClient(mod.username, mod.password) - mod.client.OnConnect(func() { + mod.chat.OnConnect(func() { mod.connectedState <- true }) - mod.client.OnUserJoinMessage(func(msg twitch.UserJoinMessage) { + mod.chat.OnUserJoinMessage(func(msg twitch.UserJoinMessage) { ev := bees.Event{ Bee: mod.Name(), Name: "join", @@ -153,7 +218,7 @@ func (mod *TwitchBee) Run(eventChan chan bees.Event) { } eventChan <- ev }) - mod.client.OnUserPartMessage(func(msg twitch.UserPartMessage) { + mod.chat.OnUserPartMessage(func(msg twitch.UserPartMessage) { ev := bees.Event{ Bee: mod.Name(), Name: "part", @@ -173,7 +238,7 @@ func (mod *TwitchBee) Run(eventChan chan bees.Event) { eventChan <- ev }) - mod.client.OnPrivateMessage(func(msg twitch.PrivateMessage) { + mod.chat.OnPrivateMessage(func(msg twitch.PrivateMessage) { ev := bees.Event{ Bee: mod.Name(), Name: "message", @@ -201,10 +266,20 @@ func (mod *TwitchBee) Run(eventChan chan bees.Event) { connected := false mod.ContextSet("connected", &connected) - mod.rejoin() + var err error + mod.client, err = helix.NewClient(&helix.Options{ + ClientID: mod.clientId, + }) + if err != nil { + mod.LogErrorf("Failed connecting to Twitch API: %v", err) + return + } + go mod.monitorFollows(eventChan) + go func() { mod.Logln("Connecting to Twitch") - err := mod.client.Connect() + mod.rejoin() + err := mod.chat.Connect() if err != nil { mod.LogErrorf("Failed to connect to Twitch: %v", err) } @@ -222,7 +297,7 @@ func (mod *TwitchBee) Run(eventChan chan bees.Event) { } case <-mod.SigChan: - mod.client.Disconnect() + mod.chat.Disconnect() return default: @@ -235,6 +310,7 @@ func (mod *TwitchBee) Run(eventChan chan bees.Event) { func (mod *TwitchBee) ReloadOptions(options bees.BeeOptions) { mod.SetOptions(options) + options.Bind("client_id", &mod.clientId) options.Bind("username", &mod.username) options.Bind("password", &mod.password) options.Bind("channels", &mod.channels) diff --git a/bees/twitchbee/twitchbeefactory.go b/bees/twitchbee/twitchbeefactory.go index c53c668c..7d317c5c 100644 --- a/bees/twitchbee/twitchbeefactory.go +++ b/bees/twitchbee/twitchbeefactory.go @@ -67,6 +67,12 @@ func (factory *TwitchBeeFactory) LogoColor() string { // Options returns the options available to configure this Bee. func (factory *TwitchBeeFactory) Options() []bees.BeeOptionDescriptor { opts := []bees.BeeOptionDescriptor{ + { + Name: "client_id", + Description: "Your Twitch Client ID", + Type: "string", + Mandatory: true, + }, { Name: "username", Description: "Your Twitch username", @@ -130,6 +136,18 @@ func (factory *TwitchBeeFactory) Events() []bees.EventDescriptor { }, }, }, + { + Namespace: factory.Name(), + Name: "follow", + Description: "A user followed you", + Options: []bees.PlaceholderDescriptor{ + { + Name: "user", + Description: "The user that followed you", + Type: "string", + }, + }, + }, { Namespace: factory.Name(), Name: "join", diff --git a/go.mod b/go.mod index c0a1133a..e307793e 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,7 @@ require ( github.com/muesli/goefa v0.0.0-20180507204150-08d8ee2555d2 github.com/muesli/kmeans v0.0.0-20190917235210-80dfc71e6c5a // indirect github.com/muesli/smolder v0.0.0-20190505085143-9c21fc7135ee + github.com/nicklaw5/helix v0.5.7 github.com/nlopes/slack v0.6.0 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/odwrtw/transmission v0.0.0-20170515140915-08885b3058e7 diff --git a/go.sum b/go.sum index 2851242c..91465b56 100644 --- a/go.sum +++ b/go.sum @@ -61,7 +61,6 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ= -github.com/gempir/go-twitch-irc v1.1.0 h1:Q9gQGI/3yJzYwlYDlFsGJzWfpaqubMExfmBXNpOC6W0= github.com/gempir/go-twitch-irc/v2 v2.2.2 h1:uzinel2qApXL1UVfr3QcZ3dJsf+YU+PaUp0qJk03qNo= github.com/gempir/go-twitch-irc/v2 v2.2.2/go.mod h1:0HXoEr9l7gNjwajosptV0w0xGpHeU6gsD7JDlfvjTYI= github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 h1:u8AQ9bPa9oC+8/A/jlWouakhIvkFfuxgIIRjiy8av7I= @@ -170,6 +169,8 @@ github.com/muesli/kmeans v0.0.0-20190917235210-80dfc71e6c5a h1:TfqSfRTR/uTo2h3Af github.com/muesli/kmeans v0.0.0-20190917235210-80dfc71e6c5a/go.mod h1:DchXqcxIXcInFMlUlAAcmjNYiDEPoIQQdDxlWlELyxM= github.com/muesli/smolder v0.0.0-20190505085143-9c21fc7135ee h1:owp+45s+e2xN1fFUHIld1X+2ZtHSUjXKkgEr7iwq7u8= github.com/muesli/smolder v0.0.0-20190505085143-9c21fc7135ee/go.mod h1:vBtOJlVGxLheofBQdKVQGaG40aJQUwr6Lh4PVliqebY= +github.com/nicklaw5/helix v0.5.7 h1:DvNyoKkuLYrqZv5/yugL18Ud99UeQoXzzAsg4OwU8uY= +github.com/nicklaw5/helix v0.5.7/go.mod h1:nRcok4VLg8ONQYW/iXBZ24wcfiJjTlDbhgk0ZatOrUY= github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=