package xmpp import ( "context" "crypto/tls" "sync" "sync/atomic" "time" "fmt" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/store/kvstore" xmppClient "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp" "github.com/mattermost/mattermost/server/public/plugin" ) // xmppBridge handles syncing messages between Mattermost and XMPP type xmppBridge struct { logger logger.Logger api plugin.API kvstore kvstore.KVStore xmppClient *xmppClient.Client // Connection management connected atomic.Bool ctx context.Context cancel context.CancelFunc // Current configuration config *config.Configuration configMu sync.RWMutex // Channel mappings cache channelMappings map[string]string mappingsMu sync.RWMutex } // NewBridge creates a new XMPP bridge func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *config.Configuration) pluginModel.Bridge { ctx, cancel := context.WithCancel(context.Background()) bridge := &xmppBridge{ logger: log, api: api, kvstore: kvstore, ctx: ctx, cancel: cancel, channelMappings: make(map[string]string), config: cfg, } // Initialize XMPP client with configuration if cfg.EnableSync && cfg.XMPPServerURL != "" && cfg.XMPPUsername != "" && cfg.XMPPPassword != "" { bridge.xmppClient = bridge.createXMPPClient(cfg) } return bridge } // createXMPPClient creates an XMPP client with the given configuration func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) *xmppClient.Client { // Create TLS config based on certificate verification setting tlsConfig := &tls.Config{ InsecureSkipVerify: cfg.XMPPInsecureSkipVerify, } return xmppClient.NewClientWithTLS( cfg.XMPPServerURL, cfg.XMPPUsername, cfg.XMPPPassword, cfg.GetXMPPResource(), "", // remoteID not needed for bridge user tlsConfig, ) } // UpdateConfiguration updates the bridge configuration func (b *xmppBridge) UpdateConfiguration(newConfig any) error { cfg, ok := newConfig.(*config.Configuration) if !ok { return fmt.Errorf("invalid configuration type") } b.configMu.Lock() oldConfig := b.config b.config = cfg // Initialize or update XMPP client with new configuration if cfg.EnableSync { if cfg.XMPPServerURL == "" || cfg.XMPPUsername == "" || cfg.XMPPPassword == "" { b.configMu.Unlock() return fmt.Errorf("XMPP server URL, username, and password are required when sync is enabled") } b.xmppClient = b.createXMPPClient(cfg) } else { b.xmppClient = nil } b.configMu.Unlock() // Check if we need to restart the bridge due to configuration changes wasConnected := b.connected.Load() needsRestart := oldConfig != nil && !oldConfig.Equals(cfg) && wasConnected // Log the configuration change if needsRestart { b.logger.LogInfo("Configuration changed, restarting bridge", "old_config", oldConfig, "new_config", cfg) } else { b.logger.LogInfo("Configuration updated", "config", cfg) } if needsRestart { b.logger.LogInfo("Configuration changed, restarting bridge") // Stop the bridge if err := b.Stop(); err != nil { b.logger.LogWarn("Error stopping bridge during restart", "error", err) } // Start the bridge with new configuration if err := b.Start(); err != nil { b.logger.LogError("Failed to restart bridge with new configuration", "error", err) return fmt.Errorf("failed to restart bridge: %w", err) } } return nil } // Start initializes the bridge and connects to XMPP func (b *xmppBridge) Start() error { b.logger.LogDebug("Starting Mattermost to XMPP bridge") b.configMu.RLock() config := b.config b.configMu.RUnlock() if config == nil { return fmt.Errorf("bridge configuration not set") } // Print the configuration for debugging b.logger.LogDebug("Bridge configuration", "config", config) if !config.EnableSync { b.logger.LogInfo("XMPP sync is disabled, bridge will not start") return nil } b.logger.LogInfo("Starting Mattermost to XMPP bridge", "xmpp_server", config.XMPPServerURL, "username", config.XMPPUsername) // Connect to XMPP server if err := b.connectToXMPP(); err != nil { return fmt.Errorf("failed to connect to XMPP server: %w", err) } // Load and join mapped channels if err := b.loadAndJoinMappedChannels(); err != nil { b.logger.LogWarn("Failed to join some mapped channels", "error", err) } // Start connection monitor go b.connectionMonitor() b.logger.LogInfo("Mattermost to XMPP bridge started successfully") return nil } // Stop shuts down the bridge func (b *xmppBridge) Stop() error { b.logger.LogInfo("Stopping Mattermost to XMPP bridge") if b.cancel != nil { b.cancel() } if b.xmppClient != nil { if err := b.xmppClient.Disconnect(); err != nil { b.logger.LogWarn("Error disconnecting from XMPP server", "error", err) } } b.connected.Store(false) b.logger.LogInfo("Mattermost to XMPP bridge stopped") return nil } // connectToXMPP establishes connection to the XMPP server func (b *xmppBridge) connectToXMPP() error { if b.xmppClient == nil { return fmt.Errorf("XMPP client is not initialized") } b.logger.LogDebug("Connecting to XMPP server") err := b.xmppClient.Connect() if err != nil { b.connected.Store(false) return fmt.Errorf("failed to connect to XMPP server: %w", err) } b.connected.Store(true) b.logger.LogInfo("Successfully connected to XMPP server") // Set online presence after successful connection if err := b.xmppClient.SetOnlinePresence(); err != nil { b.logger.LogWarn("Failed to set online presence", "error", err) // Don't fail the connection for presence issues } else { b.logger.LogDebug("Set bridge user online presence") } return nil } // loadAndJoinMappedChannels loads channel mappings and joins corresponding XMPP rooms func (b *xmppBridge) loadAndJoinMappedChannels() error { b.logger.LogDebug("Loading and joining mapped channels") // Get all channel mappings from KV store mappings, err := b.getAllChannelMappings() if err != nil { return fmt.Errorf("failed to load channel mappings: %w", err) } if len(mappings) == 0 { b.logger.LogInfo("No channel mappings found, no rooms to join") return nil } b.logger.LogInfo("Found channel mappings, joining XMPP rooms", "count", len(mappings)) // Join each mapped room for channelID, roomJID := range mappings { if err := b.joinXMPPRoom(channelID, roomJID); err != nil { b.logger.LogWarn("Failed to join room", "channel_id", channelID, "room_jid", roomJID, "error", err) } } return nil } // joinXMPPRoom joins an XMPP room and updates the local cache func (b *xmppBridge) joinXMPPRoom(channelID, roomJID string) error { if !b.connected.Load() { return fmt.Errorf("not connected to XMPP server") } err := b.xmppClient.JoinRoom(roomJID) if err != nil { return fmt.Errorf("failed to join XMPP room: %w", err) } b.logger.LogInfo("Joined XMPP room", "channel_id", channelID, "room_jid", roomJID) // Update local cache b.mappingsMu.Lock() b.channelMappings[channelID] = roomJID b.mappingsMu.Unlock() return nil } // getAllChannelMappings retrieves all channel mappings from KV store func (b *xmppBridge) getAllChannelMappings() (map[string]string, error) { if b.kvstore == nil { return nil, fmt.Errorf("KV store not initialized") } mappings := make(map[string]string) // Get all keys with the channel mapping prefix keys, err := b.kvstore.ListKeysWithPrefix(0, 1000, kvstore.KeyPrefixChannelMapping) if err != nil { return nil, fmt.Errorf("failed to list channel mapping keys: %w", err) } // Load each mapping for _, key := range keys { roomJIDBytes, err := b.kvstore.Get(key) if err != nil { b.logger.LogWarn("Failed to load mapping for key", "key", key, "error", err) continue } // Extract channel ID from the key channelID := kvstore.ExtractChannelIDFromKey(key) if channelID == "" { b.logger.LogWarn("Failed to extract channel ID from key", "key", key) continue } mappings[channelID] = string(roomJIDBytes) } return mappings, nil } // connectionMonitor monitors the XMPP connection func (b *xmppBridge) connectionMonitor() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-b.ctx.Done(): return case <-ticker.C: if err := b.checkConnection(); err != nil { b.logger.LogWarn("XMPP connection check failed", "error", err) b.handleReconnection() } } } } // checkConnection verifies the XMPP connection is still active func (b *xmppBridge) checkConnection() error { if !b.connected.Load() { return fmt.Errorf("not connected") } return b.xmppClient.TestConnection() } // handleReconnection attempts to reconnect to XMPP and rejoin rooms func (b *xmppBridge) handleReconnection() { b.configMu.RLock() config := b.config b.configMu.RUnlock() if config == nil || !config.EnableSync { return } b.logger.LogInfo("Attempting to reconnect to XMPP server") b.connected.Store(false) if b.xmppClient != nil { b.xmppClient.Disconnect() } // Retry connection with exponential backoff maxRetries := 3 for i := range maxRetries { backoff := time.Duration(1<