2024-03-10 16:38:18 +00:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.sr.ht/~ashkeel/containers/sync"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
|
|
"github.com/nicklaw5/helix/v2"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
|
|
"git.sr.ht/~ashkeel/strimertul/database"
|
2024-03-12 23:50:59 +00:00
|
|
|
"git.sr.ht/~ashkeel/strimertul/twitch"
|
|
|
|
"git.sr.ht/~ashkeel/strimertul/twitch/alerts"
|
|
|
|
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
|
|
|
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
|
|
|
"git.sr.ht/~ashkeel/strimertul/twitch/timers"
|
2024-03-10 16:38:18 +00:00
|
|
|
"git.sr.ht/~ashkeel/strimertul/webserver"
|
|
|
|
)
|
|
|
|
|
|
|
|
var json = jsoniter.ConfigFastest
|
|
|
|
|
|
|
|
type Manager struct {
|
2024-03-12 23:50:59 +00:00
|
|
|
client *Client
|
2024-03-10 16:38:18 +00:00
|
|
|
}
|
|
|
|
|
2024-03-12 23:50:59 +00:00
|
|
|
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Manager, error) {
|
2024-03-10 16:38:18 +00:00
|
|
|
// Get Twitch config
|
|
|
|
var config twitch.Config
|
|
|
|
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
|
|
|
|
if !errors.Is(err, database.ErrEmptyKey) {
|
|
|
|
return nil, fmt.Errorf("failed to get twitch config: %w", err)
|
|
|
|
}
|
|
|
|
config.Enabled = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create new client
|
2024-03-12 23:50:59 +00:00
|
|
|
clientContext, cancel := context.WithCancel(ctx)
|
|
|
|
client, err := newClient(clientContext, config, db, server, logger)
|
2024-03-10 16:38:18 +00:00
|
|
|
if err != nil {
|
2024-03-12 23:50:59 +00:00
|
|
|
cancel()
|
2024-03-10 16:38:18 +00:00
|
|
|
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
manager := &Manager{
|
|
|
|
client: client,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Listen for client config changes
|
2024-03-12 23:50:59 +00:00
|
|
|
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
|
2024-03-10 16:38:18 +00:00
|
|
|
var newConfig twitch.Config
|
|
|
|
if err := json.UnmarshalFromString(value, &newConfig); err != nil {
|
|
|
|
logger.Error("Failed to decode Twitch integration config", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-12 23:50:59 +00:00
|
|
|
cancel()
|
|
|
|
|
2024-03-10 16:38:18 +00:00
|
|
|
var updatedClient *Client
|
2024-03-12 23:50:59 +00:00
|
|
|
clientContext, cancel = context.WithCancel(ctx)
|
|
|
|
updatedClient, err = newClient(clientContext, newConfig, db, server, logger)
|
2024-03-10 16:38:18 +00:00
|
|
|
if err != nil {
|
|
|
|
logger.Error("Could not create twitch client with new config, keeping old", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// New client works, replace old
|
|
|
|
updatedClient.Merge(manager.client)
|
|
|
|
manager.client = updatedClient
|
|
|
|
|
|
|
|
logger.Info("Reloaded/updated Twitch integration")
|
2024-03-12 23:50:59 +00:00
|
|
|
}); err != nil {
|
2024-03-10 16:38:18 +00:00
|
|
|
logger.Error("Could not setup twitch config reload subscription", zap.Error(err))
|
|
|
|
}
|
|
|
|
|
|
|
|
return manager, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Manager) Client() *Client {
|
|
|
|
return m.client
|
|
|
|
}
|
|
|
|
|
|
|
|
type Client struct {
|
|
|
|
Config *sync.RWSync[twitch.Config]
|
|
|
|
DB database.Database
|
|
|
|
API *helix.Client
|
|
|
|
User helix.User
|
|
|
|
Logger *zap.Logger
|
|
|
|
|
2024-03-12 22:39:18 +00:00
|
|
|
Chat *chat.Module
|
|
|
|
Alerts *alerts.Module
|
|
|
|
Timers *timers.Module
|
|
|
|
|
2024-03-10 16:38:18 +00:00
|
|
|
eventSub *eventsub.Client
|
|
|
|
server *webserver.WebServer
|
|
|
|
ctx context.Context
|
|
|
|
cancel context.CancelFunc
|
|
|
|
|
|
|
|
restart chan bool
|
|
|
|
streamOnline *sync.RWSync[bool]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) Merge(old *Client) {
|
|
|
|
// Copy bot instance and some params
|
|
|
|
c.streamOnline.Set(old.streamOnline.Get())
|
|
|
|
c.ensureRoute()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hacky function to deal with sync issues when restarting client
|
|
|
|
func (c *Client) ensureRoute() {
|
|
|
|
if c.Config.Get().Enabled {
|
|
|
|
c.server.RegisterRoute(twitch.CallbackRoute, c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-12 23:50:59 +00:00
|
|
|
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Client, error) {
|
2024-03-10 16:38:18 +00:00
|
|
|
// Create Twitch client
|
|
|
|
client := &Client{
|
|
|
|
Config: sync.NewRWSync(config),
|
|
|
|
DB: db,
|
|
|
|
Logger: logger.With(zap.String("service", "twitch")),
|
|
|
|
restart: make(chan bool, 128),
|
|
|
|
streamOnline: sync.NewRWSync(false),
|
|
|
|
ctx: ctx,
|
|
|
|
server: server,
|
|
|
|
}
|
|
|
|
|
2024-03-12 22:39:18 +00:00
|
|
|
if !config.Enabled {
|
|
|
|
return client, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
client.API, err = twitch.GetHelixAPI(db)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
|
|
|
}
|
2024-03-10 16:38:18 +00:00
|
|
|
|
2024-03-12 22:39:18 +00:00
|
|
|
server.RegisterRoute(twitch.CallbackRoute, client)
|
2024-03-10 16:38:18 +00:00
|
|
|
|
2024-03-12 22:39:18 +00:00
|
|
|
if userClient, err := twitch.GetUserClient(db, true); err == nil {
|
|
|
|
users, err := userClient.GetUsers(&helix.UsersParams{})
|
|
|
|
if err != nil {
|
|
|
|
client.Logger.Error("Failed looking up user", zap.Error(err))
|
|
|
|
} else if len(users.Data.Users) < 1 {
|
|
|
|
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
|
|
|
} else {
|
|
|
|
client.Logger.Info("Twitch user identified", zap.String("user", users.Data.Users[0].ID))
|
|
|
|
client.User = users.Data.Users[0]
|
|
|
|
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, logger)
|
2024-03-10 16:38:18 +00:00
|
|
|
if err != nil {
|
2024-03-12 22:39:18 +00:00
|
|
|
client.Logger.Error("Failed to setup EventSub", zap.Error(err))
|
2024-03-10 16:38:18 +00:00
|
|
|
}
|
|
|
|
|
2024-03-12 22:39:18 +00:00
|
|
|
tpl := client.GetTemplateEngine()
|
|
|
|
client.Chat = chat.Setup(ctx, db, userClient, client.User, logger, tpl)
|
|
|
|
client.Alerts = alerts.Setup(ctx, db, logger, tpl)
|
|
|
|
client.Timers = timers.Setup(ctx, db, logger)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
client.Logger.Warn("Twitch user not identified, this will break most features")
|
2024-03-10 16:38:18 +00:00
|
|
|
}
|
|
|
|
|
2024-03-12 22:39:18 +00:00
|
|
|
go client.runStatusPoll()
|
|
|
|
|
2024-03-12 23:50:59 +00:00
|
|
|
go func() {
|
|
|
|
<-ctx.Done()
|
|
|
|
server.UnregisterRoute(twitch.CallbackRoute)
|
|
|
|
}()
|
|
|
|
|
2024-03-10 16:38:18 +00:00
|
|
|
return client, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) runStatusPoll() {
|
|
|
|
c.Logger.Info("Started polling for stream status")
|
|
|
|
for {
|
|
|
|
// Make sure we're configured and connected properly first
|
|
|
|
if !c.Config.Get().Enabled {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if streamer is online, if possible
|
|
|
|
func() {
|
|
|
|
status, err := c.API.GetStreams(&helix.StreamsParams{
|
2024-03-12 23:50:59 +00:00
|
|
|
UserIDs: []string{c.User.ID},
|
2024-03-10 16:38:18 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
c.Logger.Error("Error checking stream status", zap.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
c.streamOnline.Set(len(status.Data.Streams) > 0)
|
|
|
|
|
|
|
|
err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams)
|
|
|
|
if err != nil {
|
|
|
|
c.Logger.Warn("Error saving stream info", zap.Error(err))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Wait for next poll (or cancellation)
|
|
|
|
select {
|
|
|
|
case <-c.ctx.Done():
|
|
|
|
return
|
|
|
|
case <-time.After(60 * time.Second):
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|