2021-05-14 11:15:38 +00:00
|
|
|
package twitch
|
|
|
|
|
|
|
|
import (
|
2021-11-23 10:34:02 +00:00
|
|
|
"errors"
|
2021-11-19 18:37:42 +00:00
|
|
|
"fmt"
|
2022-06-16 22:51:27 +00:00
|
|
|
"time"
|
2021-11-19 18:37:42 +00:00
|
|
|
|
2022-06-16 22:51:27 +00:00
|
|
|
"git.sr.ht/~hamcha/containers"
|
2022-11-23 21:22:49 +00:00
|
|
|
lru "github.com/hashicorp/golang-lru"
|
2021-11-24 10:55:12 +00:00
|
|
|
jsoniter "github.com/json-iterator/go"
|
2022-01-27 15:49:18 +00:00
|
|
|
"github.com/nicklaw5/helix/v2"
|
2022-02-01 11:35:34 +00:00
|
|
|
"github.com/strimertul/strimertul/modules/database"
|
2022-01-27 15:49:18 +00:00
|
|
|
"go.uber.org/zap"
|
2021-11-24 10:55:12 +00:00
|
|
|
|
2021-11-23 10:34:02 +00:00
|
|
|
"github.com/strimertul/strimertul/modules"
|
|
|
|
"github.com/strimertul/strimertul/modules/loyalty"
|
2021-05-14 11:15:38 +00:00
|
|
|
)
|
|
|
|
|
2022-11-23 15:34:49 +00:00
|
|
|
var json = jsoniter.ConfigFastest
|
|
|
|
|
2021-05-14 11:15:38 +00:00
|
|
|
type Client struct {
|
2022-11-23 21:22:49 +00:00
|
|
|
Config Config
|
|
|
|
Bot *Bot
|
|
|
|
db *database.DBModule
|
|
|
|
API *helix.Client
|
|
|
|
logger *zap.Logger
|
|
|
|
manager *modules.Manager
|
|
|
|
eventCache *lru.Cache
|
2021-11-24 10:55:12 +00:00
|
|
|
|
2022-06-16 22:51:27 +00:00
|
|
|
restart chan bool
|
|
|
|
streamOnline *containers.RWSync[bool]
|
2021-05-14 11:15:38 +00:00
|
|
|
}
|
|
|
|
|
2021-12-09 10:45:10 +00:00
|
|
|
func Register(manager *modules.Manager) error {
|
2022-02-01 11:35:34 +00:00
|
|
|
db, ok := manager.Modules["db"].(*database.DBModule)
|
2021-11-23 10:34:02 +00:00
|
|
|
if !ok {
|
2021-12-09 10:45:10 +00:00
|
|
|
return errors.New("db module not found")
|
2021-05-14 11:15:38 +00:00
|
|
|
}
|
|
|
|
|
2022-01-27 15:49:18 +00:00
|
|
|
logger := manager.Logger(modules.ModuleTwitch)
|
2021-11-23 10:34:02 +00:00
|
|
|
|
2022-11-23 21:22:49 +00:00
|
|
|
eventCache, err := lru.New(128)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not create LRU cache for events: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-11-19 18:37:42 +00:00
|
|
|
// Get Twitch config
|
|
|
|
var config Config
|
2022-11-23 21:22:49 +00:00
|
|
|
err = db.GetJSON(ConfigKey, &config)
|
2021-11-19 18:37:42 +00:00
|
|
|
if err != nil {
|
2022-06-16 22:51:27 +00:00
|
|
|
if !errors.Is(err, database.ErrEmptyKey) {
|
|
|
|
return fmt.Errorf("failed to get twitch config: %w", err)
|
|
|
|
}
|
|
|
|
config.Enabled = false
|
2021-11-19 18:37:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-14 11:15:38 +00:00
|
|
|
// Create Twitch client
|
2021-11-19 18:37:42 +00:00
|
|
|
client := &Client{
|
2022-06-16 22:51:27 +00:00
|
|
|
Config: config,
|
|
|
|
db: db,
|
|
|
|
logger: logger,
|
|
|
|
restart: make(chan bool, 128),
|
|
|
|
streamOnline: containers.NewRWSync(false),
|
2022-11-23 21:22:49 +00:00
|
|
|
manager: manager,
|
|
|
|
eventCache: eventCache,
|
2021-11-23 10:34:02 +00:00
|
|
|
}
|
|
|
|
|
2021-11-24 10:55:12 +00:00
|
|
|
// Listen for config changes
|
2022-11-25 13:16:20 +00:00
|
|
|
err = db.SubscribeKey(func(value string) {
|
|
|
|
err := json.UnmarshalFromString(value, &config)
|
|
|
|
if err != nil {
|
|
|
|
logger.Error("failed to unmarshal config", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
api, err := client.getHelixAPI()
|
|
|
|
if err != nil {
|
|
|
|
logger.Warn("failed to create new twitch client, keeping old credentials", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
client.API = api
|
|
|
|
|
|
|
|
logger.Info("reloaded/updated Twitch API")
|
|
|
|
}, ConfigKey)
|
|
|
|
if err != nil {
|
|
|
|
client.logger.Error("could not setup twitch config reload subscription", zap.Error(err))
|
|
|
|
}
|
|
|
|
|
|
|
|
err = db.SubscribeKey(func(value string) {
|
|
|
|
var twitchBotConfig BotConfig
|
|
|
|
err := json.UnmarshalFromString(value, &twitchBotConfig)
|
|
|
|
if err != nil {
|
|
|
|
logger.Error("failed to unmarshal config", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = client.Bot.Client.Disconnect()
|
|
|
|
if err != nil {
|
|
|
|
logger.Warn("failed to disconnect from Twitch IRC", zap.Error(err))
|
|
|
|
}
|
|
|
|
if client.Config.EnableBot {
|
|
|
|
if err := client.startBot(manager); err != nil {
|
|
|
|
if !errors.Is(err, database.ErrEmptyKey) {
|
|
|
|
logger.Error("failed to re-create bot", zap.Error(err))
|
2022-06-16 22:51:27 +00:00
|
|
|
}
|
|
|
|
}
|
2021-11-24 10:55:12 +00:00
|
|
|
}
|
2022-11-25 13:16:20 +00:00
|
|
|
client.restart <- true
|
|
|
|
logger.Info("reloaded/restarted Twitch bot")
|
|
|
|
}, BotConfigKey)
|
|
|
|
if err != nil {
|
|
|
|
client.logger.Error("could not setup twitch bot config reload subscription", zap.Error(err))
|
|
|
|
}
|
2021-11-24 10:55:12 +00:00
|
|
|
|
2022-11-23 21:22:49 +00:00
|
|
|
if config.Enabled {
|
|
|
|
client.API, err = client.getHelixAPI()
|
|
|
|
if err != nil {
|
|
|
|
client.logger.Error("failed to create twitch client", zap.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if client.Config.EnableBot {
|
|
|
|
if err := client.startBot(manager); err != nil {
|
|
|
|
if !errors.Is(err, database.ErrEmptyKey) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
go client.runStatusPoll()
|
|
|
|
go client.connectWebsocket()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
if client.Config.EnableBot && client.Bot != nil {
|
|
|
|
err := client.RunBot()
|
|
|
|
if err != nil {
|
|
|
|
logger.Error("failed to connect to Twitch IRC", zap.Error(err))
|
|
|
|
// Wait for config change before retrying
|
|
|
|
<-client.restart
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
<-client.restart
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2021-12-09 10:45:10 +00:00
|
|
|
manager.Modules[modules.ModuleTwitch] = client
|
|
|
|
|
2022-11-23 21:22:49 +00:00
|
|
|
// If loyalty module is enabled, set-up loyalty commands
|
|
|
|
if loyaltyManager, ok := client.manager.Modules[modules.ModuleLoyalty].(*loyalty.Manager); ok && client.Bot != nil {
|
|
|
|
client.Bot.SetupLoyalty(loyaltyManager)
|
|
|
|
}
|
|
|
|
|
2021-12-09 10:45:10 +00:00
|
|
|
return nil
|
2021-05-14 11:15:38 +00:00
|
|
|
}
|
2021-11-23 10:34:02 +00:00
|
|
|
|
2022-06-16 22:51:27 +00:00
|
|
|
func (c *Client) runStatusPoll() {
|
|
|
|
c.logger.Info("status poll started")
|
|
|
|
for {
|
|
|
|
// Wait for next poll
|
|
|
|
time.Sleep(60 * time.Second)
|
|
|
|
|
2022-06-18 23:52:17 +00:00
|
|
|
// Make sure we're configured and connected properly first
|
|
|
|
if !c.Config.Enabled || c.Bot == nil || c.Bot.config.Channel == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-06-16 22:51:27 +00:00
|
|
|
// Check if streamer is online, if possible
|
|
|
|
func() {
|
|
|
|
status, err := c.API.GetStreams(&helix.StreamsParams{
|
2022-11-23 15:34:49 +00:00
|
|
|
UserLogins: []string{c.Bot.config.Channel}, // TODO Replace with something non bot dependant
|
2022-06-16 22:51:27 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Error("Error checking stream status", zap.Error(err))
|
|
|
|
} else {
|
|
|
|
c.streamOnline.Set(len(status.Data.Streams) > 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = c.db.PutJSON(StreamInfoKey, status.Data.Streams)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Warn("Error saving stream info", zap.Error(err))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) startBot(manager *modules.Manager) error {
|
|
|
|
// Get Twitch bot config
|
|
|
|
var twitchBotConfig BotConfig
|
|
|
|
err := c.db.GetJSON(BotConfigKey, &twitchBotConfig)
|
|
|
|
if err != nil {
|
|
|
|
if !errors.Is(err, database.ErrEmptyKey) {
|
|
|
|
return fmt.Errorf("failed to get bot config: %w", err)
|
|
|
|
}
|
|
|
|
c.Config.EnableBot = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create and run IRC bot
|
|
|
|
c.Bot = NewBot(c, twitchBotConfig)
|
|
|
|
|
|
|
|
// If loyalty module is enabled, set-up loyalty commands
|
|
|
|
if loyaltyManager, ok := manager.Modules[modules.ModuleLoyalty].(*loyalty.Manager); ok && c.Bot != nil {
|
|
|
|
c.Bot.SetupLoyalty(loyaltyManager)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-11-23 21:22:49 +00:00
|
|
|
func (c *Client) getHelixAPI() (*helix.Client, error) {
|
|
|
|
redirectURI, err := c.getRedirectURI()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-11-24 10:55:12 +00:00
|
|
|
// Create Twitch client
|
|
|
|
api, err := helix.NewClient(&helix.Options{
|
2022-11-23 21:22:49 +00:00
|
|
|
ClientID: c.Config.APIClientID,
|
|
|
|
ClientSecret: c.Config.APIClientSecret,
|
|
|
|
RedirectURI: redirectURI,
|
2021-11-24 10:55:12 +00:00
|
|
|
})
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-09 09:46:14 +00:00
|
|
|
func (c *Client) Status() modules.ModuleStatus {
|
2021-12-09 10:45:10 +00:00
|
|
|
if !c.Config.Enabled {
|
|
|
|
return modules.ModuleStatus{
|
|
|
|
Enabled: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-09 09:46:14 +00:00
|
|
|
return modules.ModuleStatus{
|
|
|
|
Enabled: true,
|
|
|
|
Working: c.Bot != nil && c.Bot.Client != nil,
|
|
|
|
Data: struct{}{},
|
|
|
|
StatusString: "",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-23 10:34:02 +00:00
|
|
|
func (c *Client) Close() error {
|
|
|
|
return c.Bot.Client.Disconnect()
|
|
|
|
}
|