diff --git a/cmd_syncTwitchFollows.go b/cmd_syncTwitchFollows.go new file mode 100644 index 0000000..3c2eafe --- /dev/null +++ b/cmd_syncTwitchFollows.go @@ -0,0 +1,159 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/Luzifer/go_helpers/v2/str" + "github.com/Luzifer/lounge-control/sioclient" +) + +func init() { + registerCommand("sync-twitch-follows", commandSyncTwitchFollows) +} + +func commandSyncTwitchFollows(args []string) handlerFunc { + channelAct := func(lobbyID int, action, twitchName string) error { + msg, err := sioclient.NewMessage(sioclient.MessageTypeEvent, 0, "input", map[string]interface{}{ + "text": fmt.Sprintf("/%s #%s", action, twitchName), + "target": lobbyID, + }) + if err != nil { + return errors.Wrap(err, "Unable to compose join message") + } + + if err = msg.Send(client); err != nil { + return errors.Wrap(err, "Unable to send join message") + } + + // Twitch limits the number of actions, so we need an arbitrary delay + time.Sleep(750 * time.Millisecond) + return nil + } + + return addGenericHandler(func(pType string, msg *sioclient.Message) error { + if pType != "init" { + return nil + } + + network := initData.NetworkByNameOrUUID(cfg.Network) + if network == nil { + return errors.New("Network not found") + } + + // Find lobby to send commands to + var lobby *channel + for _, c := range network.Channels { + if c.Type == "lobby" { + lobby = &c + break + } + } + + if lobby == nil { + return errors.New("Unable to find lobby for network") + } + + // Get configured nickname (must match Twitch nick) + var user = network.Nick + log.WithField("username", user).Info("Synchronizing with twitch user") + + // Convert username into user ID + req, _ := http.NewRequest("GET", fmt.Sprintf("https://api.twitch.tv/kraken/users?login=%s", user), nil) + req.Header.Set("Accept", "application/vnd.twitchtv.v5+json") + req.Header.Set("Client-ID", twitchClientID) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return errors.Wrap(err, "Unable to get user ID for Twitch user") + } + defer resp.Body.Close() + + var respObjUsers struct { + Users []struct { + ID string `json:"_id"` + } `json:"users"` + } + if err = json.NewDecoder(resp.Body).Decode(&respObjUsers); err != nil { + return errors.Wrap(err, "Unable to read Twitch response") + } + + if l := len(respObjUsers.Users); l != 1 { + return errors.Errorf("Received invalid number of user IDs: %d", l) + } + + var userID = respObjUsers.Users[0].ID + + // Retrieve follows + req, _ = http.NewRequest("GET", fmt.Sprintf("https://api.twitch.tv/kraken/users/%s/follows/channels?limit=100", userID), nil) + req.Header.Set("Accept", "application/vnd.twitchtv.v5+json") + req.Header.Set("Client-ID", twitchClientID) + + resp, err = http.DefaultClient.Do(req) + if err != nil { + return errors.Wrap(err, "Unable to get follows for Twitch user") + } + defer resp.Body.Close() + + var respObjFollows struct { + Follows []struct { + Channel struct { + Name string `json:"name"` + } `json:"channel"` + } `json:"follows"` + } + if err = json.NewDecoder(resp.Body).Decode(&respObjFollows); err != nil { + return errors.Wrap(err, "Unable to read Twitch response") + } + + // Compare channel list and act on them + var ( + expectedChannels = []string{user} + presentChannels []string + ) + + for _, c := range network.Channels { + if c.Type != "channel" { + continue + } + presentChannels = append(presentChannels, strings.TrimPrefix(c.Name, "#")) + } + + for _, f := range respObjFollows.Follows { + expectedChannels = append(expectedChannels, f.Channel.Name) + } + + // Join new channels + for _, cn := range expectedChannels { + if str.StringInSlice(cn, presentChannels) { + continue + } + log.WithField("channel", cn).Info("Joining new channel") + if err = channelAct(lobby.ID, "join", cn); err != nil { + return errors.Wrap(err, "Unable to execute channel action") + } + } + + // Leave unexpected channels + for _, cn := range presentChannels { + if str.StringInSlice(cn, expectedChannels) { + log.WithField("channel", cn).Debug("Retaining channel") + continue + } + log.WithField("channel", cn).Info("Leaving channel") + if err = channelAct(lobby.ID, "part", cn); err != nil { + return errors.Wrap(err, "Unable to execute channel action") + } + } + + interrupt <- os.Interrupt + return nil + }) +} diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..c474813 --- /dev/null +++ b/constants.go @@ -0,0 +1,3 @@ +package main + +const twitchClientID = "53govsefmz3c7pd5ev8slxlphtfo1j" diff --git a/go.mod b/go.mod index ab0f36a..6464755 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 replace github.com/Luzifer/lounge-control/sioclient => ./sioclient require ( + github.com/Luzifer/go_helpers/v2 v2.10.0 github.com/Luzifer/lounge-control/sioclient v0.0.0-00010101000000-000000000000 github.com/Luzifer/rconfig/v2 v2.2.1 github.com/gorilla/websocket v1.4.2 // indirect diff --git a/go.sum b/go.sum index b8a3863..c6f2880 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,16 @@ +github.com/Luzifer/go_helpers v1.4.0 h1:Pmm058SbYewfnpP1CHda/zERoAqYoZFiBHF4l8k03Ko= +github.com/Luzifer/go_helpers/v2 v2.10.0 h1:rA3945P6tH1PKRdcVD+nAdAWojfgwX8wQm/jjUNPmfg= +github.com/Luzifer/go_helpers/v2 v2.10.0/go.mod h1:ZnWxPjyCdQ4rZP3kNiMSUW/7FigU1X9Rz8XopdJ5ZCU= github.com/Luzifer/rconfig v1.2.0 h1:waD1sqasGVSQSrExpLrQ9Q1JmMaltrS391VdOjWXP/I= github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg= github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/leekchan/gtf v0.0.0-20190214083521-5fba33c5b00b/go.mod h1:thNruaSwydMhkQ8dXzapABF9Sc1Tz08ZBcDdgott9RA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=