diff --git a/main.go b/main.go index 962b16b..e838faf 100644 --- a/main.go +++ b/main.go @@ -114,7 +114,6 @@ func main() { fmt.Printf("It appears this is your first time running %s! Please go to http://%s and make sure to configure anything you want!\n\n", AppTitle, DefaultBind) } - // Get Stulbe config, if enabled if moduleConfig.EnableStulbe { stulbeManager, err := stulbe.Initialize(manager) if err != nil { diff --git a/modules/stulbe/client.go b/modules/stulbe/client.go index c828689..33e0679 100644 --- a/modules/stulbe/client.go +++ b/modules/stulbe/client.go @@ -15,6 +15,8 @@ type Manager struct { Client *stulbe.Client db *database.DB logger logrus.FieldLogger + + restart chan bool } func Initialize(manager *modules.Manager) (*Manager, error) { @@ -44,19 +46,57 @@ func Initialize(manager *modules.Manager) (*Manager, error) { // Create manager stulbeManager := &Manager{ - Client: stulbeClient, - db: db, - logger: logger, + Client: stulbeClient, + db: db, + logger: logger, + restart: make(chan bool), } // Register module manager.Modules[modules.ModuleStulbe] = stulbeManager + // Receive key updates go func() { - err := stulbeManager.ReceiveEvents() - logger.WithError(err).Error("Stulbe subscription died unexpectedly!") + for { + err := stulbeManager.ReceiveEvents() + if err != nil { + logger.WithError(err).Error("Stulbe subscription died unexpectedly!") + // Wait for config change before retrying + <-stulbeManager.restart + } + } }() + // Listen for config changes + go db.Subscribe(context.Background(), func(changed []database.ModifiedKV) error { + for _, kv := range changed { + if kv.Key == ConfigKey { + var config Config + err := db.GetJSON(ConfigKey, &config) + if err != nil { + logger.WithError(err).Warn("Failed to get config") + continue + } + + client, err := stulbe.NewClient(stulbe.ClientOptions{ + Endpoint: config.Endpoint, + Username: config.Username, + AuthKey: config.AuthKey, + Logger: logger, + }) + if err != nil { + logger.WithError(err).Warn("Failed to update stulbe client, keeping old settings") + } else { + stulbeManager.Client.Close() + stulbeManager.Client = client + stulbeManager.restart <- true + logger.Info("updated/restarted stulbe client") + } + } + } + return nil + }, ConfigKey) + return stulbeManager, nil } @@ -66,11 +106,16 @@ func (m *Manager) ReceiveEvents() error { return err } for { - kv := <-chn - err := m.db.PutKey(kv.Key, []byte(kv.Value)) - if err != nil { - return err + select { + case kv := <-chn: + err := m.db.PutKey(kv.Key, []byte(kv.Value)) + if err != nil { + return err + } + case <-m.restart: + return nil } + } } @@ -99,7 +144,7 @@ func (m *Manager) ReplicateKey(prefix string) error { m.logger.WithFields(logrus.Fields{ "prefix": prefix, - }).Debug("synched to remote") + }).Debug("synced to remote") // Subscribe to local datastore and update remote on change return m.db.Subscribe(context.Background(), func(pairs []database.ModifiedKV) error { diff --git a/modules/twitch/client.go b/modules/twitch/client.go index 4092fa2..bb7b5df 100644 --- a/modules/twitch/client.go +++ b/modules/twitch/client.go @@ -1,9 +1,12 @@ package twitch import ( + "context" "errors" "fmt" + jsoniter "github.com/json-iterator/go" + "github.com/strimertul/strimertul/modules" "github.com/strimertul/strimertul/modules/database" "github.com/strimertul/strimertul/modules/loyalty" @@ -17,6 +20,8 @@ type Client struct { db *database.DB API *helix.Client logger logrus.FieldLogger + + restart chan bool } func NewClient(manager *modules.Manager) (*Client, error) { @@ -35,27 +40,16 @@ func NewClient(manager *modules.Manager) (*Client, error) { } // Create Twitch client - api, err := helix.NewClient(&helix.Options{ - ClientID: config.APIClientID, - ClientSecret: config.APIClientSecret, - }) + api, err := getHelixAPI(config.APIClientID, config.APIClientSecret) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create twitch client: %w", err) } - // Get access token - resp, err := api.RequestAppAccessToken([]string{"user:read:email"}) - if err != nil { - return nil, err - } - // Set the access token on the client - api.SetAppAccessToken(resp.Data.AccessToken) - log.Info("obtained API access token") - client := &Client{ - db: db, - API: api, - logger: log, + db: db, + API: api, + logger: log, + restart: make(chan bool), } // Get Twitch bot config @@ -68,8 +62,13 @@ func NewClient(manager *modules.Manager) (*Client, error) { // Create and run IRC bot client.Bot = NewBot(client, twitchBotConfig) go func() { - if err := client.Bot.Connect(); err != nil { - log.WithError(err).Error("failed to connect to Twitch IRC") + for { + err := client.RunBot() + if err != nil { + log.WithError(err).Error("failed to connect to Twitch IRC") + // Wait for config change before retrying + <-client.restart + } } }() @@ -80,9 +79,78 @@ func NewClient(manager *modules.Manager) (*Client, error) { manager.Modules[modules.ModuleTwitch] = client + // Listen for config changes + go db.Subscribe(context.Background(), func(changed []database.ModifiedKV) error { + for _, kv := range changed { + switch kv.Key { + case ConfigKey: + err := jsoniter.ConfigFastest.Unmarshal(kv.Data, &config) + if err != nil { + log.WithError(err).Error("failed to unmarshal config") + continue + } + api, err := getHelixAPI(config.APIClientID, config.APIClientSecret) + if err != nil { + log.WithError(err).Warn("failed to create new twitch client, keeping old credentials") + continue + } + client.API = api + log.Info("reloaded/updated Twitch API") + case BotConfigKey: + err := jsoniter.ConfigFastest.Unmarshal(kv.Data, &twitchBotConfig) + if err != nil { + log.WithError(err).Error("failed to unmarshal config") + continue + } + err = client.Bot.Client.Disconnect() + if err != nil { + log.WithError(err).Warn("failed to disconnect from Twitch IRC") + } + client.Bot = NewBot(client, twitchBotConfig) + client.restart <- true + log.Info("reloaded/restarted Twitch bot") + } + } + return nil + }, ConfigKey, BotConfigKey) + return client, nil } +func getHelixAPI(clientID string, clientSecret string) (*helix.Client, error) { + // Create Twitch client + api, err := helix.NewClient(&helix.Options{ + ClientID: clientID, + ClientSecret: clientSecret, + }) + if err != nil { + return nil, err + } + + // Get access token + resp, err := api.RequestAppAccessToken([]string{"user:read:email"}) + if err != nil { + return nil, err + } + // Set the access token on the client + api.SetAppAccessToken(resp.Data.AccessToken) + + return api, nil +} + +func (c *Client) RunBot() error { + cherr := make(chan error) + go func() { + cherr <- c.Bot.Connect() + }() + select { + case <-c.restart: + return nil + case err := <-cherr: + return err + } +} + func (c *Client) Close() error { return c.Bot.Client.Disconnect() }