2021-05-14 11:15:38 +00:00
|
|
|
package twitch
|
|
|
|
|
|
|
|
import (
|
2022-12-03 15:16:59 +00:00
|
|
|
"context"
|
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
|
|
|
|
2023-11-10 20:36:15 +00:00
|
|
|
"git.sr.ht/~ashkeel/containers/sync"
|
2023-03-31 09:43:33 +00:00
|
|
|
lru "github.com/hashicorp/golang-lru/v2"
|
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-12-04 13:45:34 +00:00
|
|
|
"go.uber.org/zap"
|
|
|
|
|
2023-11-10 20:36:15 +00:00
|
|
|
"git.sr.ht/~ashkeel/strimertul/database"
|
|
|
|
"git.sr.ht/~ashkeel/strimertul/webserver"
|
2021-05-14 11:15:38 +00:00
|
|
|
)
|
|
|
|
|
2022-11-23 15:34:49 +00:00
|
|
|
var json = jsoniter.ConfigFastest
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
type Manager struct {
|
|
|
|
client *Client
|
|
|
|
cancelSubs func()
|
2021-05-14 11:15:38 +00:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func NewManager(db *database.LocalDBClient, server *webserver.WebServer, logger *zap.Logger) (*Manager, error) {
|
2023-05-19 13:07:32 +00:00
|
|
|
// Get Twitch config
|
2021-11-19 18:37:42 +00:00
|
|
|
var config Config
|
2022-12-03 15:16:59 +00:00
|
|
|
if err := db.GetJSON(ConfigKey, &config); err != nil {
|
2022-06-16 22:51:27 +00:00
|
|
|
if !errors.Is(err, database.ErrEmptyKey) {
|
2022-12-03 15:16:59 +00:00
|
|
|
return nil, fmt.Errorf("failed to get twitch config: %w", err)
|
2022-06-16 22:51:27 +00:00
|
|
|
}
|
|
|
|
config.Enabled = false
|
2021-11-19 18:37:42 +00:00
|
|
|
}
|
|
|
|
|
2023-05-19 13:07:32 +00:00
|
|
|
// Get Twitch bot config
|
2023-05-25 17:48:32 +00:00
|
|
|
botConfig := defaultBotConfig()
|
2022-12-03 15:16:59 +00:00
|
|
|
if err := db.GetJSON(BotConfigKey, &botConfig); err != nil {
|
|
|
|
if !errors.Is(err, database.ErrEmptyKey) {
|
|
|
|
return nil, fmt.Errorf("failed to get bot config: %w", err)
|
|
|
|
}
|
|
|
|
config.EnableBot = false
|
2021-11-23 10:34:02 +00:00
|
|
|
}
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
// Create new client
|
|
|
|
client, err := newClient(config, db, server, logger)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if config.EnableBot {
|
|
|
|
client.Bot = newBot(client, botConfig)
|
|
|
|
go client.Bot.Connect()
|
|
|
|
}
|
|
|
|
|
|
|
|
manager := &Manager{
|
|
|
|
client: client,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Listen for client config changes
|
2024-02-25 13:46:59 +00:00
|
|
|
cancelConfigSub, err := db.SubscribeKey(ConfigKey, func(value string) {
|
2022-12-03 15:16:59 +00:00
|
|
|
var newConfig Config
|
|
|
|
if err := json.UnmarshalFromString(value, &newConfig); err != nil {
|
2023-04-23 00:26:46 +00:00
|
|
|
logger.Error("Failed to decode Twitch integration config", zap.Error(err))
|
2022-11-25 13:16:20 +00:00
|
|
|
return
|
|
|
|
}
|
2022-12-03 15:16:59 +00:00
|
|
|
|
|
|
|
var updatedClient *Client
|
|
|
|
updatedClient, err = newClient(newConfig, db, server, logger)
|
2022-11-25 13:16:20 +00:00
|
|
|
if err != nil {
|
2023-04-23 00:26:46 +00:00
|
|
|
logger.Error("Could not create twitch client with new config, keeping old", zap.Error(err))
|
2022-11-25 13:16:20 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
err = manager.client.Close()
|
|
|
|
if err != nil {
|
2023-04-23 00:26:46 +00:00
|
|
|
logger.Warn("Twitch client could not close cleanly", zap.Error(err))
|
2022-12-03 15:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// New client works, replace old
|
|
|
|
updatedClient.Merge(manager.client)
|
|
|
|
manager.client = updatedClient
|
|
|
|
|
2023-04-23 00:26:46 +00:00
|
|
|
logger.Info("Reloaded/updated Twitch integration")
|
2022-11-25 19:57:52 +00:00
|
|
|
})
|
2022-11-25 13:16:20 +00:00
|
|
|
if err != nil {
|
2023-04-23 00:26:46 +00:00
|
|
|
logger.Error("Could not setup twitch config reload subscription", zap.Error(err))
|
2022-11-25 13:16:20 +00:00
|
|
|
}
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
// Listen for bot config changes
|
2024-02-25 13:46:59 +00:00
|
|
|
cancelBotSub, err := db.SubscribeKey(BotConfigKey, func(value string) {
|
2023-05-25 17:48:32 +00:00
|
|
|
newBotConfig := defaultBotConfig()
|
2022-12-03 15:16:59 +00:00
|
|
|
if err := json.UnmarshalFromString(value, &newBotConfig); err != nil {
|
2023-04-23 00:26:46 +00:00
|
|
|
logger.Error("Failed to decode bot config", zap.Error(err))
|
2022-11-25 13:16:20 +00:00
|
|
|
return
|
|
|
|
}
|
2022-12-03 15:16:59 +00:00
|
|
|
|
2022-12-22 12:35:30 +00:00
|
|
|
if manager.client.Bot != nil {
|
|
|
|
err = manager.client.Bot.Close()
|
|
|
|
if err != nil {
|
2023-04-23 00:26:46 +00:00
|
|
|
manager.client.logger.Warn("Failed to disconnect old bot from Twitch IRC", zap.Error(err))
|
2022-12-22 12:35:30 +00:00
|
|
|
}
|
2022-11-25 13:16:20 +00:00
|
|
|
}
|
2022-12-03 15:16:59 +00:00
|
|
|
|
2022-12-24 13:34:46 +00:00
|
|
|
if manager.client.Config.Get().EnableBot {
|
|
|
|
bot := newBot(manager.client, newBotConfig)
|
2022-12-03 15:16:59 +00:00
|
|
|
go bot.Connect()
|
2022-12-24 13:34:46 +00:00
|
|
|
manager.client.Bot = bot
|
|
|
|
} else {
|
|
|
|
manager.client.Bot = nil
|
2021-11-24 10:55:12 +00:00
|
|
|
}
|
2022-12-03 15:16:59 +00:00
|
|
|
|
2023-04-24 14:18:15 +00:00
|
|
|
manager.client.logger.Info("Reloaded/restarted Twitch bot")
|
2022-11-25 19:57:52 +00:00
|
|
|
})
|
2022-11-25 13:16:20 +00:00
|
|
|
if err != nil {
|
2023-04-23 00:26:46 +00:00
|
|
|
client.logger.Error("Could not setup twitch bot config reload subscription", zap.Error(err))
|
2022-11-25 13:16:20 +00:00
|
|
|
}
|
2021-11-24 10:55:12 +00:00
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
manager.cancelSubs = func() {
|
|
|
|
if cancelConfigSub != nil {
|
|
|
|
cancelConfigSub()
|
|
|
|
}
|
|
|
|
if cancelBotSub != nil {
|
|
|
|
cancelBotSub()
|
2022-11-23 21:22:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
return manager, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Client() *Client {
|
|
|
|
return m.client
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Close() error {
|
|
|
|
m.cancelSubs()
|
|
|
|
|
|
|
|
if err := m.client.Close(); err != nil {
|
|
|
|
return err
|
2022-11-23 21:22:49 +00:00
|
|
|
}
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type Client struct {
|
2022-12-04 13:45:34 +00:00
|
|
|
Config *sync.RWSync[Config]
|
2022-12-03 15:16:59 +00:00
|
|
|
Bot *Bot
|
|
|
|
db *database.LocalDBClient
|
|
|
|
API *helix.Client
|
2023-02-02 20:24:14 +00:00
|
|
|
User helix.User
|
2022-12-03 15:16:59 +00:00
|
|
|
logger *zap.Logger
|
2023-03-31 09:43:33 +00:00
|
|
|
eventCache *lru.Cache[string, time.Time]
|
2023-05-31 12:49:45 +00:00
|
|
|
server *webserver.WebServer
|
2022-12-03 15:16:59 +00:00
|
|
|
ctx context.Context
|
|
|
|
cancel context.CancelFunc
|
|
|
|
|
|
|
|
restart chan bool
|
2022-12-04 13:45:34 +00:00
|
|
|
streamOnline *sync.RWSync[bool]
|
2022-12-03 15:16:59 +00:00
|
|
|
savedSubscriptions map[string]bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) Merge(old *Client) {
|
|
|
|
// Copy bot instance and some params
|
|
|
|
c.streamOnline.Set(old.streamOnline.Get())
|
|
|
|
c.Bot = old.Bot
|
2022-12-22 12:35:30 +00:00
|
|
|
c.ensureRoute()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hacky function to deal with sync issues when restarting client
|
|
|
|
func (c *Client) ensureRoute() {
|
|
|
|
if c.Config.Get().Enabled {
|
|
|
|
c.server.RegisterRoute(CallbackRoute, c)
|
|
|
|
}
|
2022-12-03 15:16:59 +00:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func newClient(config Config, db *database.LocalDBClient, server *webserver.WebServer, logger *zap.Logger) (*Client, error) {
|
2023-03-31 09:43:33 +00:00
|
|
|
eventCache, err := lru.New[string, time.Time](128)
|
2022-12-03 15:16:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not create LRU cache for events: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create Twitch client
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
client := &Client{
|
2022-12-04 13:45:34 +00:00
|
|
|
Config: sync.NewRWSync(config),
|
2022-12-03 15:16:59 +00:00
|
|
|
db: db,
|
|
|
|
logger: logger.With(zap.String("service", "twitch")),
|
|
|
|
restart: make(chan bool, 128),
|
2022-12-04 13:45:34 +00:00
|
|
|
streamOnline: sync.NewRWSync(false),
|
2022-12-03 15:16:59 +00:00
|
|
|
eventCache: eventCache,
|
|
|
|
savedSubscriptions: make(map[string]bool),
|
|
|
|
ctx: ctx,
|
|
|
|
cancel: cancel,
|
|
|
|
server: server,
|
|
|
|
}
|
|
|
|
|
|
|
|
baseurl, err := client.baseURL()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if config.Enabled {
|
|
|
|
api, err := getHelixAPI(config, baseurl)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
2022-11-23 21:22:49 +00:00
|
|
|
}
|
2022-12-03 15:16:59 +00:00
|
|
|
|
|
|
|
client.API = api
|
|
|
|
server.RegisterRoute(CallbackRoute, client)
|
|
|
|
|
2023-03-05 19:11:19 +00:00
|
|
|
if userClient, err := client.GetUserClient(true); err == nil {
|
2023-02-02 20:24:14 +00:00
|
|
|
users, err := userClient.GetUsers(&helix.UsersParams{})
|
|
|
|
if err != nil {
|
2023-04-23 00:26:46 +00:00
|
|
|
client.logger.Error("Failed looking up user", zap.Error(err))
|
2023-02-06 09:36:00 +00:00
|
|
|
} else if len(users.Data.Users) < 1 {
|
2023-04-23 00:26:46 +00:00
|
|
|
client.logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
2023-02-06 09:36:00 +00:00
|
|
|
} else {
|
|
|
|
client.User = users.Data.Users[0]
|
2023-02-15 23:21:34 +00:00
|
|
|
go client.eventSubLoop(userClient)
|
2023-02-02 20:24:14 +00:00
|
|
|
}
|
|
|
|
} else {
|
2023-04-23 00:26:46 +00:00
|
|
|
client.logger.Warn("Twitch user not identified, this will break most features")
|
2023-02-02 20:24:14 +00:00
|
|
|
}
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
go client.runStatusPoll()
|
|
|
|
}
|
2022-11-23 21:22:49 +00:00
|
|
|
|
2022-11-30 18:15:47 +00:00
|
|
|
return client, 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() {
|
2023-04-19 13:27:13 +00:00
|
|
|
c.logger.Info("Started polling for stream status")
|
2022-06-16 22:51:27 +00:00
|
|
|
for {
|
2022-06-18 23:52:17 +00:00
|
|
|
// Make sure we're configured and connected properly first
|
2022-12-03 15:16:59 +00:00
|
|
|
if !c.Config.Get().Enabled || c.Bot == nil || c.Bot.Config.Channel == "" {
|
2022-06-18 23:52:17 +00:00
|
|
|
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-30 18:15:47 +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))
|
2023-02-01 10:53:27 +00:00
|
|
|
return
|
2022-06-16 22:51:27 +00:00
|
|
|
}
|
2024-02-25 13:58:35 +00:00
|
|
|
c.streamOnline.Set(len(status.Data.Streams) > 0)
|
2022-06-16 22:51:27 +00:00
|
|
|
|
|
|
|
err = c.db.PutJSON(StreamInfoKey, status.Data.Streams)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Warn("Error saving stream info", zap.Error(err))
|
|
|
|
}
|
|
|
|
}()
|
2023-02-02 20:24:14 +00:00
|
|
|
|
|
|
|
// Wait for next poll (or cancellation)
|
|
|
|
select {
|
|
|
|
case <-c.ctx.Done():
|
|
|
|
return
|
|
|
|
case <-time.After(60 * time.Second):
|
|
|
|
}
|
2022-06-16 22:51:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
func getHelixAPI(config Config, baseurl string) (*helix.Client, error) {
|
|
|
|
redirectURI := getRedirectURI(baseurl)
|
2022-11-23 21:22:49 +00:00
|
|
|
|
2021-11-24 10:55:12 +00:00
|
|
|
// Create Twitch client
|
|
|
|
api, err := helix.NewClient(&helix.Options{
|
2022-11-30 18:15:47 +00:00
|
|
|
ClientID: config.APIClientID,
|
|
|
|
ClientSecret: config.APIClientSecret,
|
2022-11-23 21:22:49 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-12-03 15:16:59 +00:00
|
|
|
func (c *Client) baseURL() (string, error) {
|
|
|
|
var severConfig struct {
|
|
|
|
Bind string `json:"bind"`
|
2021-11-24 10:55:12 +00:00
|
|
|
}
|
2022-12-03 15:16:59 +00:00
|
|
|
err := c.db.GetJSON("http/config", &severConfig)
|
|
|
|
return severConfig.Bind, err
|
2021-11-24 10:55:12 +00:00
|
|
|
}
|
|
|
|
|
2022-11-30 18:15:47 +00:00
|
|
|
func (c *Client) IsLive() bool {
|
|
|
|
return c.streamOnline.Get()
|
2021-12-09 09:46:14 +00:00
|
|
|
}
|
|
|
|
|
2021-11-23 10:34:02 +00:00
|
|
|
func (c *Client) Close() error {
|
2022-12-03 15:16:59 +00:00
|
|
|
c.server.UnregisterRoute(CallbackRoute)
|
|
|
|
defer c.cancel()
|
|
|
|
|
2022-12-22 12:35:30 +00:00
|
|
|
if c.Bot != nil {
|
|
|
|
if err := c.Bot.Close(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-12-03 15:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2021-11-23 10:34:02 +00:00
|
|
|
}
|