289 lines
8.8 KiB
Go
289 lines
8.8 KiB
Go
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")
|
|
|
|
// 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"))
|
|
|
|
// Create WebSocket client (pass the API client for vote tracking)
|
|
m.wsClient = NewWebSocketClient(apiURL, token, wrappedCallback, m.client, enableRoomCodeImage, imageDelay, plaintextDelay, m.log)
|
|
|
|
// 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
|
|
}
|