Files

289 lines
8.8 KiB
Go
Raw Permalink Normal View History

2025-11-01 10:40:53 -04:00
package jackbox
import (
"fmt"
"sync"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
)
// Manager handles the Jackbox integration lifecycle
type Manager struct {
client *Client
webhookServer *WebhookServer
wsClient *WebSocketClient
config config.Config
log *logrus.Entry
enabled bool
useWebSocket bool
messageCallback func(string)
muted bool
mu sync.RWMutex
}
// NewManager creates a new Jackbox manager
func NewManager(cfg config.Config, log *logrus.Entry) *Manager {
return &Manager{
config: cfg,
log: log,
}
}
// Initialize sets up the Jackbox client and webhook server or WebSocket client
func (m *Manager) Initialize() error {
// Check if Jackbox integration is enabled
m.enabled = m.config.Viper().GetBool("jackbox.Enabled")
if !m.enabled {
m.log.Info("Jackbox integration is disabled")
return nil
}
m.log.Info("Initializing Jackbox integration...")
// Get configuration values
apiURL := m.config.Viper().GetString("jackbox.APIURL")
adminPassword := m.config.Viper().GetString("jackbox.AdminPassword")
m.useWebSocket = m.config.Viper().GetBool("jackbox.UseWebSocket")
// Validate configuration
if apiURL == "" {
return fmt.Errorf("jackbox.APIURL is required when Jackbox integration is enabled")
}
if adminPassword == "" {
return fmt.Errorf("jackbox.AdminPassword is required when Jackbox integration is enabled")
}
// Create Jackbox API client
m.client = NewClient(apiURL, adminPassword, m.log)
// Authenticate with the API
if err := m.client.Authenticate(); err != nil {
return fmt.Errorf("failed to authenticate with Jackbox API: %w", err)
}
m.log.Info("Jackbox integration initialized successfully")
return nil
}
// StartWebhookServer starts the webhook server with the provided message callback
func (m *Manager) StartWebhookServer(messageCallback func(string)) error {
if !m.enabled {
return nil
}
// Use WebSocket if enabled, otherwise fall back to webhook
if m.useWebSocket {
return m.startWebSocketClient(messageCallback)
}
webhookPort := m.config.Viper().GetInt("jackbox.WebhookPort")
webhookSecret := m.config.Viper().GetString("jackbox.WebhookSecret")
if webhookSecret == "" {
return fmt.Errorf("jackbox.WebhookSecret is required when using webhooks")
}
if webhookPort == 0 {
webhookPort = 3001
}
// Wrap the callback to check mute status
wrappedCallback := func(message string) {
if m.IsMuted() {
m.log.Debugf("Jackbox message suppressed (muted): %s", message)
return
}
messageCallback(message)
}
m.webhookServer = NewWebhookServer(webhookPort, webhookSecret, wrappedCallback, m.log)
return m.webhookServer.Start()
}
// startWebSocketClient starts the WebSocket client connection
func (m *Manager) startWebSocketClient(messageCallback func(string)) error {
apiURL := m.config.Viper().GetString("jackbox.APIURL")
// Store the callback for use in monitoring
m.messageCallback = messageCallback
// Wrap the callback to check mute status
wrappedCallback := func(message string) {
if m.IsMuted() {
m.log.Debugf("Jackbox message suppressed (muted): %s", message)
return
}
messageCallback(message)
}
// Set wrapped callback on client for vote broadcasts
m.client.SetMessageCallback(wrappedCallback)
// Get JWT token from client
token := m.client.GetToken()
if token == "" {
return fmt.Errorf("no JWT token available, authentication may have failed")
}
// Get EnableRoomCodeImage setting from config (defaults to false)
enableRoomCodeImage := m.config.Viper().GetBool("jackbox.EnableRoomCodeImage")
2026-02-07 12:37:21 -05:00
// Get configurable delays for room code broadcast
imageDelay := time.Duration(m.config.Viper().GetInt("jackbox.RoomCodeImageDelay")) * time.Second
plaintextDelay := time.Duration(m.config.Viper().GetInt("jackbox.RoomCodePlaintextDelay")) * time.Second
if plaintextDelay == 0 {
plaintextDelay = 29 * time.Second // default
}
m.log.Infof("Room code delays: imageDelay=%v, plaintextDelay=%v (raw config: image=%d, plaintext=%d)",
imageDelay, plaintextDelay,
m.config.Viper().GetInt("jackbox.RoomCodeImageDelay"),
m.config.Viper().GetInt("jackbox.RoomCodePlaintextDelay"))
2025-11-01 10:40:53 -04:00
// Create WebSocket client (pass the API client for vote tracking)
2026-02-07 12:37:21 -05:00
m.wsClient = NewWebSocketClient(apiURL, token, wrappedCallback, m.client, enableRoomCodeImage, imageDelay, plaintextDelay, m.log)
2025-11-01 10:40:53 -04:00
// Connect to WebSocket
if err := m.wsClient.Connect(); err != nil {
return fmt.Errorf("failed to connect WebSocket: %w", err)
}
// Get active session and subscribe
session, err := m.client.GetActiveSession()
if err != nil {
m.log.Warnf("Could not get active session: %v", err)
m.log.Info("WebSocket connected but not subscribed to any session yet")
return nil
}
if session != nil && session.ID > 0 {
if err := m.wsClient.Subscribe(session.ID); err != nil {
m.log.Warnf("Failed to subscribe to session %d: %v", session.ID, err)
}
// Set the active session on the client for vote tracking
m.client.SetActiveSession(session.ID)
// Only announce if this is a NEW session (no games played yet)
// If games have been played, the bot is just reconnecting to an existing session
if session.GamesPlayed == 0 {
announcement := fmt.Sprintf("🎮 Game Night is starting! Session #%d", session.ID)
if messageCallback != nil {
messageCallback(announcement)
}
} else {
m.log.Infof("Reconnected to existing session #%d (%d games already played)", session.ID, session.GamesPlayed)
}
} else {
m.log.Info("No active session found, will subscribe when session becomes active")
// Start a goroutine to periodically check for active sessions
go m.monitorActiveSessions()
}
return nil
}
// monitorActiveSessions periodically checks for active sessions and subscribes
// This is a FALLBACK mechanism - only used when WebSocket events aren't working
// Normally, session.started and session.ended events should handle this
func (m *Manager) monitorActiveSessions() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
wasSubscribed := false
for {
select {
case <-ticker.C:
// Skip polling if WebSocket is disconnected (no point in polling)
if m.wsClient == nil || !m.wsClient.IsConnected() {
m.log.Debug("WebSocket disconnected, skipping session poll")
continue
}
session, err := m.client.GetActiveSession()
if err != nil {
m.log.Debugf("Error checking for active session: %v", err)
continue
}
isSubscribed := m.wsClient.IsSubscribed()
// Check if we need to subscribe to a new session (fallback if session.started wasn't received)
if !isSubscribed && session != nil && session.ID > 0 {
m.log.Warnf("Found active session %d via polling (session.started event may have been missed), subscribing...", session.ID)
if err := m.wsClient.Subscribe(session.ID); err != nil {
m.log.Warnf("Failed to subscribe to session %d: %v", session.ID, err)
} else {
m.client.SetActiveSession(session.ID)
wasSubscribed = true
// Only announce if this is a NEW session (no games played yet)
if session.GamesPlayed == 0 && !m.IsMuted() {
announcement := fmt.Sprintf("🎮 Game Night is starting! Session #%d", session.ID)
if m.messageCallback != nil {
m.messageCallback(announcement)
}
} else if session.GamesPlayed == 0 && m.IsMuted() {
m.log.Debugf("Jackbox message suppressed (muted): 🎮 Game Night is starting! Session #%d", session.ID)
}
}
}
// Check if session ended (fallback if session.ended wasn't received)
if wasSubscribed && (session == nil || session.ID == 0 || session.IsActive == 0) {
m.log.Warn("Active session ended (detected via polling, session.ended event may have been missed)")
if m.wsClient != nil {
m.wsClient.AnnounceSessionEnd()
}
wasSubscribed = false
}
}
}
}
// GetClient returns the Jackbox API client (may be nil if disabled)
func (m *Manager) GetClient() *Client {
return m.client
}
// IsEnabled returns whether Jackbox integration is enabled
func (m *Manager) IsEnabled() bool {
return m.enabled
}
// Shutdown stops the webhook server or WebSocket client
func (m *Manager) Shutdown() error {
if m.wsClient != nil {
if err := m.wsClient.Close(); err != nil {
m.log.Errorf("Error closing WebSocket client: %v", err)
}
}
if m.webhookServer != nil {
return m.webhookServer.Stop()
}
return nil
}
// SetMuted sets the mute state for Jackbox announcements
func (m *Manager) SetMuted(muted bool) {
m.mu.Lock()
defer m.mu.Unlock()
m.muted = muted
}
// ToggleMuted toggles the mute state and returns the new state
func (m *Manager) ToggleMuted() bool {
m.mu.Lock()
defer m.mu.Unlock()
m.muted = !m.muted
return m.muted
}
// IsMuted returns the current mute state
func (m *Manager) IsMuted() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.muted
}