mattermost-plugin-bridge-xmpp/server/bridge/manager.go
Felipe Martin 69a67704f4
fix: prevent dangling XMPP connections during configuration updates
- Add getConfiguration() methods to both bridges for thread-safe config access
- Refactor UpdateConfiguration() methods to prevent mutex deadlock by releasing lock before blocking operations
- Fix XMPP bridge to properly disconnect existing bridgeClient before creating new one
- Add comprehensive timeout support to XMPP client (30s connection, 10s operations, 5s ping)
- Implement proper disconnection with offline presence
- Update all interfaces to use *config.Configuration for type safety

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-04 19:04:43 +02:00

407 lines
13 KiB
Go

package bridge
import (
"fmt"
"sync"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
mmModel "github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)
// BridgeManager manages multiple bridge instances
type BridgeManager struct {
bridges map[string]model.Bridge
mu sync.RWMutex
logger logger.Logger
api plugin.API
remoteID string
}
// NewBridgeManager creates a new bridge manager
func NewBridgeManager(logger logger.Logger, api plugin.API, remoteID string) model.BridgeManager {
if logger == nil {
panic("logger cannot be nil")
}
if api == nil {
panic("plugin API cannot be nil")
}
return &BridgeManager{
bridges: make(map[string]model.Bridge),
logger: logger,
api: api,
remoteID: remoteID,
}
}
// RegisterBridge registers a bridge with the manager
func (m *BridgeManager) RegisterBridge(name string, bridge model.Bridge) error {
if name == "" {
return fmt.Errorf("bridge name cannot be empty")
}
if bridge == nil {
return fmt.Errorf("bridge cannot be nil")
}
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.bridges[name]; exists {
return fmt.Errorf("bridge '%s' is already registered", name)
}
m.bridges[name] = bridge
m.logger.LogInfo("Bridge registered", "name", name)
return nil
}
// StartBridge starts a specific bridge
func (m *BridgeManager) StartBridge(name string) error {
m.mu.RLock()
bridge, exists := m.bridges[name]
m.mu.RUnlock()
if !exists {
return fmt.Errorf("bridge '%s' is not registered", name)
}
m.logger.LogInfo("Starting bridge", "name", name)
if err := bridge.Start(); err != nil {
m.logger.LogError("Failed to start bridge", "name", name, "error", err)
return fmt.Errorf("failed to start bridge '%s': %w", name, err)
}
m.logger.LogInfo("Bridge started successfully", "name", name)
return nil
}
// StopBridge stops a specific bridge
func (m *BridgeManager) StopBridge(name string) error {
m.mu.RLock()
bridge, exists := m.bridges[name]
m.mu.RUnlock()
if !exists {
return fmt.Errorf("bridge '%s' is not registered", name)
}
m.logger.LogInfo("Stopping bridge", "name", name)
if err := bridge.Stop(); err != nil {
m.logger.LogError("Failed to stop bridge", "name", name, "error", err)
return fmt.Errorf("failed to stop bridge '%s': %w", name, err)
}
m.logger.LogInfo("Bridge stopped successfully", "name", name)
return nil
}
// UnregisterBridge removes a bridge from the manager
func (m *BridgeManager) UnregisterBridge(name string) error {
m.mu.Lock()
defer m.mu.Unlock()
bridge, exists := m.bridges[name]
if !exists {
return fmt.Errorf("bridge '%s' is not registered", name)
}
// Stop the bridge before unregistering
if bridge.IsConnected() {
if err := bridge.Stop(); err != nil {
m.logger.LogWarn("Failed to stop bridge during unregistration", "name", name, "error", err)
}
}
delete(m.bridges, name)
m.logger.LogInfo("Bridge unregistered", "name", name)
return nil
}
// GetBridge retrieves a bridge by name
func (m *BridgeManager) GetBridge(name string) (model.Bridge, error) {
m.mu.RLock()
defer m.mu.RUnlock()
bridge, exists := m.bridges[name]
if !exists {
return nil, fmt.Errorf("bridge '%s' is not registered", name)
}
return bridge, nil
}
// ListBridges returns a list of all registered bridge names
func (m *BridgeManager) ListBridges() []string {
m.mu.RLock()
defer m.mu.RUnlock()
bridges := make([]string, 0, len(m.bridges))
for name := range m.bridges {
bridges = append(bridges, name)
}
return bridges
}
// HasBridge checks if a bridge with the given name is registered
func (m *BridgeManager) HasBridge(name string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
_, exists := m.bridges[name]
return exists
}
// HasBridges checks if any bridges are registered
func (m *BridgeManager) HasBridges() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.bridges) > 0
}
// Shutdown stops and unregisters all bridges
func (m *BridgeManager) Shutdown() error {
m.mu.Lock()
defer m.mu.Unlock()
m.logger.LogInfo("Shutting down bridge manager", "bridge_count", len(m.bridges))
var errors []error
for name, bridge := range m.bridges {
if bridge.IsConnected() {
if err := bridge.Stop(); err != nil {
errors = append(errors, fmt.Errorf("failed to stop bridge '%s': %w", name, err))
m.logger.LogError("Failed to stop bridge during shutdown", "name", name, "error", err)
}
}
}
// Clear all bridges
m.bridges = make(map[string]model.Bridge)
m.logger.LogInfo("Bridge manager shutdown complete")
if len(errors) > 0 {
return fmt.Errorf("shutdown completed with errors: %v", errors)
}
return nil
}
// OnPluginConfigurationChange propagates configuration changes to all registered bridges
func (m *BridgeManager) OnPluginConfigurationChange(config *config.Configuration) error {
m.mu.RLock()
defer m.mu.RUnlock()
if len(m.bridges) == 0 {
return nil
}
m.logger.LogInfo("Plugin configuration changed, propagating to bridges", "bridge_count", len(m.bridges))
var errors []error
for name, bridge := range m.bridges {
if err := bridge.UpdateConfiguration(config); err != nil {
errors = append(errors, fmt.Errorf("failed to update configuration for bridge '%s': %w", name, err))
m.logger.LogError("Failed to update bridge configuration", "name", name, "error", err)
} else {
m.logger.LogDebug("Successfully updated bridge configuration", "name", name)
}
}
if len(errors) > 0 {
return fmt.Errorf("configuration update completed with errors: %v", errors)
}
m.logger.LogInfo("Configuration changes propagated to all bridges")
return nil
}
// CreateChannelMapping handles the creation of a channel mapping by calling the appropriate bridge
func (m *BridgeManager) CreateChannelMapping(req model.CreateChannelMappingRequest) error {
// Validate request
if err := req.Validate(); err != nil {
return fmt.Errorf("invalid mapping request: %w", err)
}
m.logger.LogDebug("Creating channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID, "user_id", req.UserID, "team_id", req.TeamID)
// Get the specific bridge
bridge, err := m.GetBridge(req.BridgeName)
if err != nil {
m.logger.LogError("Failed to get bridge", "bridge_name", req.BridgeName, "error", err)
return fmt.Errorf("failed to get bridge '%s': %w", req.BridgeName, err)
}
// Check if bridge is connected
if !bridge.IsConnected() {
return fmt.Errorf("bridge '%s' is not connected", req.BridgeName)
}
// NEW: Check if room already mapped to another channel
existingChannelID, err := bridge.GetRoomMapping(req.BridgeRoomID)
if err != nil {
m.logger.LogError("Failed to check room mapping", "bridge_room_id", req.BridgeRoomID, "error", err)
return fmt.Errorf("failed to check room mapping: %w", err)
}
if existingChannelID != "" {
m.logger.LogWarn("Room already mapped to another channel",
"bridge_room_id", req.BridgeRoomID,
"existing_channel_id", existingChannelID,
"requested_channel_id", req.ChannelID)
return fmt.Errorf("room '%s' is already mapped to channel '%s'", req.BridgeRoomID, existingChannelID)
}
// NEW: Check if room exists on target bridge
roomExists, err := bridge.RoomExists(req.BridgeRoomID)
if err != nil {
m.logger.LogError("Failed to check room existence", "bridge_room_id", req.BridgeRoomID, "error", err)
return fmt.Errorf("failed to check room existence: %w", err)
}
if !roomExists {
m.logger.LogWarn("Room does not exist on bridge",
"bridge_room_id", req.BridgeRoomID,
"bridge_name", req.BridgeName)
return fmt.Errorf("room '%s' does not exist on %s bridge", req.BridgeRoomID, req.BridgeName)
}
m.logger.LogDebug("Room validation passed",
"bridge_room_id", req.BridgeRoomID,
"bridge_name", req.BridgeName,
"room_exists", roomExists,
"already_mapped", false)
// Create the channel mapping on the receiving bridge
if err = bridge.CreateChannelMapping(req.ChannelID, req.BridgeRoomID); err != nil {
m.logger.LogError("Failed to create channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID, "error", err)
return fmt.Errorf("failed to create channel mapping for bridge '%s': %w", req.BridgeName, err)
}
mattermostBridge, err := m.GetBridge("mattermost")
if err != nil {
m.logger.LogError("Failed to get Mattermost bridge", "error", err)
return fmt.Errorf("failed to get Mattermost bridge: %w", err)
}
// Create the channel mapping in the Mattermost bridge
if err = mattermostBridge.CreateChannelMapping(req.ChannelID, req.BridgeRoomID); err != nil {
m.logger.LogError("Failed to create channel mapping in Mattermost bridge", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID, "error", err)
return fmt.Errorf("failed to create channel mapping in Mattermost bridge: %w", err)
}
// Share the channel using Mattermost's shared channels API
if err = m.shareChannel(req); err != nil {
m.logger.LogError("Failed to share channel", "channel_id", req.ChannelID, "bridge_room_id", req.BridgeRoomID, "error", err)
// Don't fail the entire operation if sharing fails, but log the error
m.logger.LogWarn("Channel mapping created but sharing failed - channel may not sync properly")
}
m.logger.LogInfo("Successfully created channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID)
return nil
}
// DeleteChannepMapping handles the deletion of a channel mapping by calling the appropriate bridges
func (m *BridgeManager) DeleteChannepMapping(req model.DeleteChannelMappingRequest) error {
// Validate request
if err := req.Validate(); err != nil {
return fmt.Errorf("invalid delete request: %w", err)
}
m.logger.LogDebug("Deleting channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "user_id", req.UserID, "team_id", req.TeamID)
// Get the specific bridge
bridge, err := m.GetBridge(req.BridgeName)
if err != nil {
m.logger.LogError("Failed to get bridge", "bridge_name", req.BridgeName, "error", err)
return fmt.Errorf("failed to get bridge '%s': %w", req.BridgeName, err)
}
// Check if bridge is connected
if !bridge.IsConnected() {
return fmt.Errorf("bridge '%s' is not connected", req.BridgeName)
}
// Delete the channel mapping from the specific bridge
if err = bridge.DeleteChannelMapping(req.ChannelID); err != nil {
m.logger.LogError("Failed to delete channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "error", err)
return fmt.Errorf("failed to delete channel mapping for bridge '%s': %w", req.BridgeName, err)
}
// Also delete from Mattermost bridge to clean up reverse mappings
mattermostBridge, err := m.GetBridge("mattermost")
if err != nil {
m.logger.LogError("Failed to get Mattermost bridge", "error", err)
return fmt.Errorf("failed to get Mattermost bridge: %w", err)
}
// Delete the channel mapping from the Mattermost bridge
if err = mattermostBridge.DeleteChannelMapping(req.ChannelID); err != nil {
m.logger.LogError("Failed to delete channel mapping from Mattermost bridge", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "error", err)
return fmt.Errorf("failed to delete channel mapping from Mattermost bridge: %w", err)
}
// Unshare the channel using Mattermost's shared channels API
if err = m.unshareChannel(req.ChannelID); err != nil {
m.logger.LogError("Failed to unshare channel", "channel_id", req.ChannelID, "error", err)
// Don't fail the entire operation if unsharing fails, but log the error
m.logger.LogWarn("Channel mapping deleted but unsharing failed - channel may still appear as shared")
}
m.logger.LogInfo("Successfully deleted channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName)
return nil
}
// shareChannel creates a shared channel configuration using the Mattermost API
func (m *BridgeManager) shareChannel(req model.CreateChannelMappingRequest) error {
if m.remoteID == "" {
return fmt.Errorf("remote ID not set - plugin not registered for shared channels")
}
// Create SharedChannel configuration
sharedChannel := &mmModel.SharedChannel{
ChannelId: req.ChannelID,
TeamId: req.TeamID,
Home: true,
ReadOnly: false,
ShareName: model.SanitizeShareName(fmt.Sprintf("bridge-%s", req.BridgeRoomID)),
ShareDisplayName: fmt.Sprintf("Bridge: %s", req.BridgeRoomID),
SharePurpose: fmt.Sprintf("Shared channel bridged to %s", req.BridgeRoomID),
ShareHeader: "test header",
CreatorId: req.UserID,
RemoteId: m.remoteID,
}
// Share the channel
sharedChannel, err := m.api.ShareChannel(sharedChannel)
if err != nil {
return fmt.Errorf("failed to share channel via API: %w", err)
}
m.logger.LogInfo("Successfully shared channel", "channel_id", req.ChannelID, "shared_channel_id", sharedChannel.ChannelId)
return nil
}
// unshareChannel removes shared channel configuration using the Mattermost API
func (m *BridgeManager) unshareChannel(channelID string) error {
// Unshare the channel
unshared, err := m.api.UnshareChannel(channelID)
if err != nil {
return fmt.Errorf("failed to unshare channel via API: %w", err)
}
if !unshared {
m.logger.LogWarn("Channel was not shared or already unshared", "channel_id", channelID)
} else {
m.logger.LogInfo("Successfully unshared channel", "channel_id", channelID)
}
return nil
}