diff --git a/cmd/xmpp-client-doctor/main.go b/cmd/xmpp-client-doctor/main.go index 35a0228..4d61774 100644 --- a/cmd/xmpp-client-doctor/main.go +++ b/cmd/xmpp-client-doctor/main.go @@ -484,7 +484,7 @@ func testXEP0077(client *xmpp.Client, config *Config) error { // This is handled asynchronously in the client Connect method time.Sleep(2 * time.Second) - // Check if server supports XEP-0077 + // Check if server supports XEP-0077 inBandReg, err := client.GetInBandRegistration() if err != nil { return fmt.Errorf("server does not support XEP-0077 In-Band Registration: %w", err) @@ -499,7 +499,7 @@ func testXEP0077(client *xmpp.Client, config *Config) error { } serverJID := client.GetJID().Domain() - + // Step 1: Test registration fields discovery start := time.Now() if config.Verbose { diff --git a/server/bridge/messagebus.go b/server/bridge/messagebus.go index 69d2f4e..70f5488 100644 --- a/server/bridge/messagebus.go +++ b/server/bridge/messagebus.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "sync" - "sync/atomic" "time" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" @@ -33,9 +32,6 @@ type messageBus struct { wg sync.WaitGroup started bool startMu sync.Mutex - - // Graceful shutdown management - draining atomic.Bool } // NewMessageBus creates a new message bus instance @@ -74,14 +70,6 @@ func (mb *messageBus) Publish(msg *model.DirectionalMessage) error { return fmt.Errorf("bridge message cannot be nil") } - // Check if we're draining - if so, silently ignore new messages - if mb.draining.Load() { - mb.logger.LogDebug("Ignoring message during shutdown drainage", - "source_bridge", msg.SourceBridge, - "channel_id", msg.SourceChannelID) - return nil - } - select { case mb.incomingMessages <- msg: mb.logger.LogDebug("Message published to bus", @@ -128,20 +116,12 @@ func (mb *messageBus) Stop() error { return nil // Already stopped } - pendingCount := len(mb.incomingMessages) - mb.logger.LogInfo("Stopping message bus", "pending_messages", pendingCount) - - // Set draining flag to prevent new messages - mb.draining.Store(true) + mb.logger.LogInfo("Stopping message bus") // Cancel context to signal shutdown mb.cancel() - // Close incoming messages channel to signal routing goroutine to finish - // The routing goroutine will process all remaining messages until channel is empty - close(mb.incomingMessages) - - // Wait for routing goroutine to finish processing all remaining messages + // Wait for routing goroutine to finish mb.wg.Wait() // Close all subscriber channels @@ -153,8 +133,11 @@ func (mb *messageBus) Stop() error { mb.subscribers = make(map[string]chan *model.DirectionalMessage) mb.subscribersMu.Unlock() + // Close incoming messages channel + close(mb.incomingMessages) + mb.started = false - mb.logger.LogInfo("Message bus stopped successfully", "drained_messages", pendingCount) + mb.logger.LogInfo("Message bus stopped successfully") return nil } diff --git a/server/bridge/xmpp/bridge.go b/server/bridge/xmpp/bridge.go index 1cba16f..e7f76f7 100644 --- a/server/bridge/xmpp/bridge.go +++ b/server/bridge/xmpp/bridge.go @@ -3,12 +3,12 @@ package xmpp import ( "context" "crypto/tls" - "fmt" - "strings" "sync" "sync/atomic" "time" + "fmt" + "github.com/mattermost/mattermost/server/public/plugin" "mellium.im/xmlstream" "mellium.im/xmpp/stanza" @@ -66,6 +66,7 @@ func NewBridge(log logger.Logger, api plugin.API, store kvstore.KVStore, cfg *co cancel: cancel, channelMappings: make(map[string]string), config: cfg, + userManager: bridge.NewUserManager(bridgeID, log), incomingMessages: make(chan *pluginModel.DirectionalMessage, defaultMessageBufferSize), bridgeID: bridgeID, remoteID: remoteID, @@ -80,9 +81,6 @@ func NewBridge(log logger.Logger, api plugin.API, store kvstore.KVStore, cfg *co b.bridgeClient = b.createXMPPClient(cfg) } - // Initialize with a default user manager - will be replaced in Start() after XEP detection - b.userManager = bridge.NewUserManager(bridgeID, log) - return b } @@ -104,64 +102,6 @@ func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) *xmppClient.Cli ) } -// createUserManager creates the appropriate user manager based on configuration -func (b *xmppBridge) createUserManager(cfg *config.Configuration, bridgeID string, log logger.Logger, store kvstore.KVStore) pluginModel.BridgeUserManager { - b.logger.LogDebug("Creating user manager", "enable_ghost_users", cfg.EnableXMPPGhostUsers, "enable_sync", cfg.EnableSync, "ghost_prefix", cfg.XMPPGhostUserPrefix) - - // Check if ghost users are enabled in configuration - if !cfg.IsGhostUserEnabled() { - b.logger.LogInfo("Ghost users disabled, using bridge user manager", "enable_ghost_users", cfg.EnableXMPPGhostUsers, "enable_sync", cfg.EnableSync) - return bridge.NewUserManager(bridgeID, log) - } - - // Check if we have a bridge client to test XEP-0077 support - if b.bridgeClient == nil { - b.logger.LogWarn("Bridge client not available, cannot check XEP-0077 support, falling back to bridge user manager") - return bridge.NewUserManager(bridgeID, log) - } - - // Check XEP-0077 server support - if supported, err := b.checkXEP0077Support(); err != nil { - b.logger.LogWarn("Failed to check XEP-0077 support, falling back to bridge user manager", "error", err) - return bridge.NewUserManager(bridgeID, log) - } else if !supported { - b.logger.LogInfo("XEP-0077 In-Band Registration not supported by server, using bridge user manager") - return bridge.NewUserManager(bridgeID, log) - } - - // Both ghost users are enabled and XEP-0077 is supported - use ghost user manager - b.logger.LogInfo("Ghost users enabled and XEP-0077 supported, using XMPP ghost user manager") - return NewXMPPUserManager(bridgeID, log, store, b.api, cfg, b.bridgeClient) -} - -// waitForCapabilityDetection waits for server capability detection to complete -func (b *xmppBridge) waitForCapabilityDetection() error { - if b.bridgeClient == nil { - return fmt.Errorf("bridge client not available") - } - - // Trigger capability detection synchronously - if err := b.bridgeClient.DetectServerCapabilities(); err != nil { - return fmt.Errorf("failed to detect server capabilities: %w", err) - } - - return nil -} - -// checkXEP0077Support checks if the XMPP server supports XEP-0077 In-Band Registration -func (b *xmppBridge) checkXEP0077Support() (bool, error) { - if b.bridgeClient == nil { - return false, fmt.Errorf("bridge client not available") - } - - // Check XEP features from the client - if b.bridgeClient.XEPFeatures == nil { - return false, fmt.Errorf("XEP features not initialized") - } - - return b.bridgeClient.XEPFeatures.InBandRegistration != nil, nil -} - // getConfiguration safely retrieves the current configuration func (b *xmppBridge) getConfiguration() *config.Configuration { b.configMu.RLock() @@ -190,9 +130,6 @@ func (b *xmppBridge) UpdateConfiguration(cfg *config.Configuration) error { b.logger.LogError("Failed to disconnect old XMPP bridge client") } b.bridgeClient = b.createXMPPClient(cfg) - - // Recreate user manager since ghost user settings or XEP support may have changed - b.userManager = b.createUserManager(cfg, b.bridgeID, b.logger, b.kvstore) } b.configMu.Unlock() @@ -237,14 +174,6 @@ func (b *xmppBridge) Start() error { return fmt.Errorf("failed to connect to XMPP server: %w", err) } - // Wait for server capability detection to complete before creating user manager - if err := b.waitForCapabilityDetection(); err != nil { - return fmt.Errorf("failed to detect server capabilities: %w", err) - } - - // Initialize proper user manager now that we're connected and server capabilities are detected - b.userManager = b.createUserManager(cfg, b.bridgeID, b.logger, b.kvstore) - // Load and join mapped channels if err := b.loadAndJoinMappedChannels(); err != nil { b.logger.LogWarn("Failed to join some mapped channels", "error", err) @@ -675,30 +604,6 @@ func (b *xmppBridge) ID() string { return b.bridgeID } -// isBridgeUserMessage checks if the incoming XMPP message is from our bridge user to prevent loops -func (b *xmppBridge) isBridgeUserMessage(msg *stanza.Message) bool { - // Skip messages from our own XMPP user to prevent loops - // In MUC, messages come back as roomJID/nickname, so we need to check the nickname/resource - bridgeJID := b.bridgeClient.GetJID() - bridgeNickname := bridgeJID.Localpart() // Use localpart as nickname - incomingResource := msg.From.Resourcepart() - - b.logger.LogDebug("Bridge user comparison details", - "bridge_jid", bridgeJID.String(), - "bridge_nickname", bridgeNickname, - "incoming_resource", incomingResource) - - // Check multiple ways this could be our bridge user: - // 1. Direct nickname match - // 2. Resource starts with "mattermost_" (common pattern) - // 3. Resource contains bridge-related identifiers - isBridgeUser := incomingResource == bridgeNickname || - strings.HasPrefix(incomingResource, "mattermost_") || - strings.Contains(incomingResource, "bridge") - - return isBridgeUser -} - // handleIncomingXMPPMessage handles incoming XMPP messages and converts them to bridge messages // //nolint:gocritic // msg parameter must match external XMPP library handler signature @@ -734,10 +639,10 @@ func (b *xmppBridge) handleIncomingXMPPMessage(msg stanza.Message, t xmlstream.T userID, displayName := b.bridgeClient.ExtractUserInfo(msg.From) - // Check if this message is from our bridge user to prevent loops - if b.isBridgeUserMessage(&msg) { - b.logger.LogDebug("Ignoring message from bridge user to prevent loops", - "incoming_message_from", msg.From.String(), + // Skip messages from our own XMPP user to prevent loops + if userID == b.bridgeClient.GetJID().String() { + b.logger.LogDebug("Skipping message from our own XMPP user to prevent loop", + "our_jid", b.bridgeClient.GetJID().String(), "source_user_id", userID) return nil } diff --git a/server/bridge/xmpp/message_handler.go b/server/bridge/xmpp/message_handler.go index 79fcc43..0a31ffc 100644 --- a/server/bridge/xmpp/message_handler.go +++ b/server/bridge/xmpp/message_handler.go @@ -58,6 +58,14 @@ func (h *xmppMessageHandler) GetSupportedMessageTypes() []string { // sendMessageToXMPP sends a message to an XMPP room func (h *xmppMessageHandler) sendMessageToXMPP(msg *pluginModel.BridgeMessage) error { + if h.bridge.bridgeClient == nil { + return fmt.Errorf("XMPP client not initialized") + } + + if !h.bridge.connected.Load() { + return fmt.Errorf("not connected to XMPP server") + } + // Get the XMPP room JID from the channel mapping roomJID, err := h.bridge.GetChannelMapping(msg.SourceChannelID) if err != nil { @@ -67,114 +75,32 @@ func (h *xmppMessageHandler) sendMessageToXMPP(msg *pluginModel.BridgeMessage) e return fmt.Errorf("channel is not mapped to any XMPP room") } - // Check if we're using ghost users (XMPPUserManager) - userManager := h.bridge.userManager - if userManager == nil { - return fmt.Errorf("user manager not available") - } - - h.logger.LogDebug("Routing message", "manager_type", fmt.Sprintf("%T", userManager), "source_user_id", msg.SourceUserID, "room_jid", roomJID) - - // Check if this is an XMPPUserManager (ghost users enabled) - if xmppUserManager, ok := userManager.(*UserManager); ok { - // Ghost users are enabled - send message through ghost user's own client - return h.sendMessageViaGhostUser(xmppUserManager, msg, roomJID) - } - - // Regular user manager - send message through bridge client - return h.sendMessageViaBridgeUser(msg, roomJID) -} - -// sendMessageViaGhostUser sends a message using a ghost user's individual XMPP client -func (h *xmppMessageHandler) sendMessageViaGhostUser(xmppUserManager *UserManager, msg *pluginModel.BridgeMessage, roomJID string) error { - // Validate source user ID for ghost user messaging - if msg.SourceUserID == "" { - return fmt.Errorf("cannot send message with ghost users: source user ID is empty") - } - - // Get or create the ghost user - bridgeUser, err := xmppUserManager.GetOrCreateUser(msg.SourceUserID, msg.SourceUserName) - if err != nil { - h.logger.LogWarn("Failed to get/create ghost user, falling back to bridge user", "source_user_id", msg.SourceUserID, "error", err) - return h.sendMessageViaBridgeUser(msg, roomJID) - } - - // Cast to XMPPUser to access XMPP-specific methods - xmppUser, ok := bridgeUser.(*User) - if !ok { - return fmt.Errorf("expected XMPPUser, got %T", bridgeUser) - } - - // Check if the ghost user is connected - if !xmppUser.IsConnected() { - h.logger.LogDebug("Ghost user not connected, attempting to connect", "user_id", msg.SourceUserID, "ghost_jid", xmppUser.GetJID()) - // TODO: Start the user if not started - this will be handled in the user manager fix - h.logger.LogWarn("Ghost user not connected, falling back to bridge user", "user_id", msg.SourceUserID, "ghost_jid", xmppUser.GetJID()) - return h.sendMessageViaBridgeUser(msg, roomJID) - } - - // Format message content for ghost user (no prefix needed since it's sent as the actual user) - content := msg.Content - - // Send message through the ghost user's own XMPP client - err = xmppUser.SendMessageToChannel(roomJID, content) - if err != nil { - h.logger.LogWarn("Failed to send message via ghost user, falling back to bridge user", "user_id", msg.SourceUserID, "ghost_jid", xmppUser.GetJID(), "error", err) - return h.sendMessageViaBridgeUser(msg, roomJID) - } - - h.logger.LogDebug("Message sent via ghost user", - "source_user_id", msg.SourceUserID, - "ghost_jid", xmppUser.GetJID(), - "room_jid", roomJID, - "content_length", len(content)) - - return nil -} - -// sendMessageViaBridgeUser sends a message using the bridge user's XMPP client -func (h *xmppMessageHandler) sendMessageViaBridgeUser(msg *pluginModel.BridgeMessage, roomJID string) error { - // Validate bridge client is available - if h.bridge.bridgeClient == nil { - return fmt.Errorf("XMPP bridge client not initialized") - } - - if !h.bridge.connected.Load() { - return fmt.Errorf("not connected to XMPP server") - } - - // Get bridge user JID - h.bridge.configMu.RLock() - bridgeJID := h.bridge.config.XMPPUsername - h.bridge.configMu.RUnlock() - - // Format message content for bridge user (prefix with original username) - content := h.formatMessageContentForBridgeUser(msg) + // Format the message content with user information + content := h.formatMessageContent(msg) // Create XMPP message request req := xmppClient.MessageRequest{ - RoomJID: roomJID, - GhostUserJID: bridgeJID, - Message: content, + RoomJID: roomJID, + Message: content, } - // Send the message through bridge client - _, err := h.bridge.bridgeClient.SendMessage(&req) + // Send the message + _, err = h.bridge.bridgeClient.SendMessage(&req) if err != nil { - return fmt.Errorf("failed to send message to XMPP room via bridge user: %w", err) + return fmt.Errorf("failed to send message to XMPP room: %w", err) } - h.logger.LogDebug("Message sent via bridge user", - "bridge_jid", bridgeJID, + h.logger.LogDebug("Message sent to XMPP room", + "channel_id", msg.SourceChannelID, "room_jid", roomJID, "content_length", len(content)) return nil } -// formatMessageContentForBridgeUser formats message content for bridge user (with username prefix) -func (h *xmppMessageHandler) formatMessageContentForBridgeUser(msg *pluginModel.BridgeMessage) string { - // Prefix with the original user name for bridge user messages +// formatMessageContent formats the message content for XMPP +func (h *xmppMessageHandler) formatMessageContent(msg *pluginModel.BridgeMessage) string { + // For messages from other bridges, prefix with the user name if msg.SourceUserName != "" { return fmt.Sprintf("<%s> %s", msg.SourceUserName, msg.Content) } diff --git a/server/bridge/xmpp/user.go b/server/bridge/xmpp/user.go index a76a676..c8891ff 100644 --- a/server/bridge/xmpp/user.go +++ b/server/bridge/xmpp/user.go @@ -14,8 +14,10 @@ import ( xmppClient "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp" ) -// User represents an XMPP user that implements the BridgeUser interface -type User struct { +// XMPPUser represents an XMPP user that implements the BridgeUser interface +// +//nolint:revive // XMPPUser is clearer than User in this context +type XMPPUser struct { // User identity id string displayName string @@ -40,8 +42,8 @@ type User struct { logger logger.Logger } -// NewXMPPUser creates a new XMPP user with specific credentials -func NewXMPPUser(id, displayName, jid, password string, cfg *config.Configuration, log logger.Logger) *User { +// NewXMPPUser creates a new XMPP user +func NewXMPPUser(id, displayName, jid string, cfg *config.Configuration, log logger.Logger) *XMPPUser { ctx, cancel := context.WithCancel(context.Background()) // Create TLS config based on certificate verification setting @@ -49,18 +51,18 @@ func NewXMPPUser(id, displayName, jid, password string, cfg *config.Configuratio InsecureSkipVerify: cfg.XMPPInsecureSkipVerify, //nolint:gosec // Allow insecure TLS for testing environments } - // Create XMPP client for this user with provided credentials + // Create XMPP client for this user client := xmppClient.NewClientWithTLS( cfg.XMPPServerURL, jid, - password, // Use the provided password (ghost password or bridge password) + cfg.XMPPPassword, // This might need to be user-specific in the future cfg.GetXMPPResource(), id, // Use user ID as remote ID tlsConfig, log, ) - return &User{ + return &XMPPUser{ id: id, displayName: displayName, jid: jid, @@ -74,7 +76,7 @@ func NewXMPPUser(id, displayName, jid, password string, cfg *config.Configuratio } // Validation -func (u *User) Validate() error { +func (u *XMPPUser) Validate() error { if u.id == "" { return fmt.Errorf("user ID cannot be empty") } @@ -94,22 +96,22 @@ func (u *User) Validate() error { } // Identity (bridge-agnostic) -func (u *User) GetID() string { +func (u *XMPPUser) GetID() string { return u.id } -func (u *User) GetDisplayName() string { +func (u *XMPPUser) GetDisplayName() string { return u.displayName } // State management -func (u *User) GetState() model.UserState { +func (u *XMPPUser) GetState() model.UserState { u.stateMu.RLock() defer u.stateMu.RUnlock() return u.state } -func (u *User) SetState(state model.UserState) error { +func (u *XMPPUser) SetState(state model.UserState) error { u.stateMu.Lock() defer u.stateMu.Unlock() @@ -123,7 +125,7 @@ func (u *User) SetState(state model.UserState) error { } // Channel operations -func (u *User) JoinChannel(channelID string) error { +func (u *XMPPUser) JoinChannel(channelID string) error { if !u.connected.Load() { return fmt.Errorf("user %s is not connected", u.id) } @@ -140,7 +142,7 @@ func (u *User) JoinChannel(channelID string) error { return nil } -func (u *User) LeaveChannel(channelID string) error { +func (u *XMPPUser) LeaveChannel(channelID string) error { if !u.connected.Load() { return fmt.Errorf("user %s is not connected", u.id) } @@ -157,18 +159,13 @@ func (u *User) LeaveChannel(channelID string) error { return nil } -func (u *User) SendMessageToChannel(channelID, message string) error { +func (u *XMPPUser) SendMessageToChannel(channelID, message string) error { if !u.connected.Load() { return fmt.Errorf("user %s is not connected", u.id) } u.logger.LogDebug("XMPP user sending message to channel", "user_id", u.id, "channel_id", channelID) - // Ensure we're joined to the room before sending the message - if err := u.EnsureJoinedToRoom(channelID); err != nil { - return fmt.Errorf("failed to ensure joined to room before sending message: %w", err) - } - // Create message request for XMPP req := xmppClient.MessageRequest{ RoomJID: channelID, @@ -186,7 +183,7 @@ func (u *User) SendMessageToChannel(channelID, message string) error { } // Connection lifecycle -func (u *User) Connect() error { +func (u *XMPPUser) Connect() error { u.logger.LogDebug("Connecting XMPP user", "user_id", u.id, "jid", u.jid) err := u.client.Connect() @@ -210,7 +207,7 @@ func (u *User) Connect() error { return nil } -func (u *User) Disconnect() error { +func (u *XMPPUser) Disconnect() error { u.logger.LogDebug("Disconnecting XMPP user", "user_id", u.id, "jid", u.jid) if u.client == nil { @@ -229,11 +226,11 @@ func (u *User) Disconnect() error { return err } -func (u *User) IsConnected() bool { +func (u *XMPPUser) IsConnected() bool { return u.connected.Load() } -func (u *User) Ping() error { +func (u *XMPPUser) Ping() error { if !u.connected.Load() { return fmt.Errorf("XMPP user %s is not connected", u.id) } @@ -245,36 +242,8 @@ func (u *User) Ping() error { return u.client.Ping() } -// CheckRoomMembership checks if the user is joined to an XMPP room -func (u *User) CheckRoomMembership(channelID string) (bool, error) { - if !u.connected.Load() { - return false, fmt.Errorf("XMPP user %s is not connected", u.id) - } - - if u.client == nil { - return false, fmt.Errorf("XMPP client not initialized for user %s", u.id) - } - - return u.client.CheckRoomMembership(channelID) -} - -// EnsureJoinedToRoom ensures the user is joined to an XMPP room, joining if necessary -func (u *User) EnsureJoinedToRoom(channelID string) error { - if !u.connected.Load() { - return fmt.Errorf("XMPP user %s is not connected", u.id) - } - - if u.client == nil { - return fmt.Errorf("XMPP client not initialized for user %s", u.id) - } - - u.logger.LogDebug("Ensuring user is joined to room", "user_id", u.id, "channel_id", channelID) - - return u.client.EnsureJoinedToRoom(channelID) -} - // CheckChannelExists checks if an XMPP room/channel exists -func (u *User) CheckChannelExists(channelID string) (bool, error) { +func (u *XMPPUser) CheckChannelExists(channelID string) (bool, error) { if !u.connected.Load() { return false, fmt.Errorf("XMPP user %s is not connected", u.id) } @@ -287,7 +256,7 @@ func (u *User) CheckChannelExists(channelID string) (bool, error) { } // Goroutine lifecycle -func (u *User) Start(ctx context.Context) error { +func (u *XMPPUser) Start(ctx context.Context) error { u.logger.LogDebug("Starting XMPP user", "user_id", u.id, "jid", u.jid) // Update context @@ -305,7 +274,7 @@ func (u *User) Start(ctx context.Context) error { return nil } -func (u *User) Stop() error { +func (u *XMPPUser) Stop() error { u.logger.LogDebug("Stopping XMPP user", "user_id", u.id, "jid", u.jid) // Cancel context to stop goroutines @@ -323,7 +292,7 @@ func (u *User) Stop() error { } // connectionMonitor monitors the XMPP connection for this user -func (u *User) connectionMonitor() { +func (u *XMPPUser) connectionMonitor() { u.logger.LogDebug("Starting connection monitor for XMPP user", "user_id", u.id) // Simple monitoring - check connection periodically @@ -359,55 +328,11 @@ func (u *User) connectionMonitor() { } // GetJID returns the XMPP JID for this user (XMPP-specific method) -func (u *User) GetJID() string { +func (u *XMPPUser) GetJID() string { return u.jid } // GetClient returns the underlying XMPP client (for advanced operations) -func (u *User) GetClient() *xmppClient.Client { +func (u *XMPPUser) GetClient() *xmppClient.Client { return u.client } - -// UpdateCredentials updates the user's JID and password for ghost user mode -// This creates a new XMPP client with the updated credentials -func (u *User) UpdateCredentials(newJID, newPassword string) error { - u.logger.LogDebug("Updating XMPP user credentials", "user_id", u.id, "old_jid", u.jid, "new_jid", newJID) - - // Disconnect existing client if connected - wasConnected := u.IsConnected() - if wasConnected { - if err := u.Disconnect(); err != nil { - u.logger.LogWarn("Error disconnecting before credential update", "user_id", u.id, "error", err) - } - } - - // Create TLS config based on certificate verification setting - tlsConfig := &tls.Config{ - InsecureSkipVerify: u.config.XMPPInsecureSkipVerify, //nolint:gosec // Allow insecure TLS for testing environments - } - - // Create new XMPP client with updated credentials - newClient := xmppClient.NewClientWithTLS( - u.config.XMPPServerURL, - newJID, - newPassword, - u.config.GetXMPPResource(), - u.id, // Use user ID as remote ID - tlsConfig, - u.logger, - ) - - // Update user fields - u.jid = newJID - u.client = newClient - - // Reconnect if we were previously connected - if wasConnected { - if err := u.Connect(); err != nil { - return fmt.Errorf("failed to reconnect after credential update: %w", err) - } - } - - u.logger.LogInfo("XMPP user credentials updated successfully", "user_id", u.id, "new_jid", newJID) - return nil -} diff --git a/server/bridge/xmpp/user_manager.go b/server/bridge/xmpp/user_manager.go deleted file mode 100644 index fe7bdd4..0000000 --- a/server/bridge/xmpp/user_manager.go +++ /dev/null @@ -1,452 +0,0 @@ -package xmpp - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "sync" - - "github.com/mattermost/mattermost/server/public/plugin" - "mellium.im/xmpp/jid" - - "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" - "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/store/kvstore" - xmppClient "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp" -) - -const ( - // KV store key prefixes for XMPP-specific data only - ghostCredPrefix = "ghost_cred_" // Stores GhostUserData (JID, password, etc.) -) - -// buildGhostUserKey generates a KV store key for ghost user data -func buildGhostUserKey(mattermostUserID string) string { - return ghostCredPrefix + mattermostUserID -} - -// GhostUserData represents persistent XMPP-specific ghost user information -type GhostUserData struct { - MattermostUserID string `json:"mattermost_user_id"` // Mattermost user ID this ghost represents - GhostJID string `json:"ghost_jid"` // XMPP JID of the ghost user - GhostPassword string `json:"ghost_password"` // XMPP password for the ghost user - Created int64 `json:"created"` // Timestamp when ghost was created -} - -// UserManager manages XMPP users using XEP-0077 ghost users ONLY -// Only stores XMPP-specific data, uses Mattermost API for user info -type UserManager struct { - bridgeType string - logger logger.Logger - kvstore kvstore.KVStore - api plugin.API // Mattermost API for user info - config *config.Configuration - configMu sync.RWMutex - bridgeClient *xmppClient.Client - ctx context.Context - cancel context.CancelFunc -} - -// NewXMPPUserManager creates a new XMPP-specific user manager for ghost users only -func NewXMPPUserManager(bridgeType string, log logger.Logger, store kvstore.KVStore, api plugin.API, cfg *config.Configuration, bridgeClient *xmppClient.Client) model.BridgeUserManager { - ctx, cancel := context.WithCancel(context.Background()) - return &UserManager{ - bridgeType: bridgeType, - logger: log, - kvstore: store, - api: api, - config: cfg, - bridgeClient: bridgeClient, - ctx: ctx, - cancel: cancel, - } -} - -// generateGhostUserJID creates a JID for a new ghost user based on Mattermost user ID and configuration -func (m *UserManager) generateGhostUserJID(mattermostUserID string) string { - m.configMu.RLock() - prefix := m.config.XMPPGhostUserPrefix - domain := m.config.GetXMPPGhostUserDomain() - m.configMu.RUnlock() - - return fmt.Sprintf("%s%s@%s", prefix, mattermostUserID, domain) -} - -// registerGhostUser registers a new ghost user via XEP-0077 In-Band Registration -func (m *UserManager) registerGhostUser(mattermostUserID, ghostJID, ghostPassword string) error { - if m.bridgeClient == nil { - return fmt.Errorf("bridge client not available for ghost user registration") - } - - m.logger.LogDebug("Registering ghost user", "jid", ghostJID, "mattermost_user_id", mattermostUserID) - - // Get In-Band Registration handler from bridge client - regHandler, err := m.bridgeClient.GetInBandRegistration() - if err != nil { - return fmt.Errorf("failed to get in-band registration handler: %w", err) - } - - // Parse the ghost JID to get the server part - ghostJIDParsed, err := jid.Parse(ghostJID) - if err != nil { - return fmt.Errorf("failed to parse ghost JID %s: %w", ghostJID, err) - } - - // Register ghost user via XEP-0077 - request := &xmppClient.RegistrationRequest{ - Username: ghostJIDParsed.Localpart(), - Password: ghostPassword, - } - - response, err := regHandler.RegisterAccount(ghostJIDParsed.Domain(), request) - if err != nil { - return fmt.Errorf("XEP-0077 registration failed for %s: %w", ghostJID, err) - } - - if !response.Success { - return fmt.Errorf("XEP-0077 registration failed for %s: %s", ghostJID, response.Error) - } - - m.logger.LogInfo("Ghost user registered successfully", "mattermost_user_id", mattermostUserID, "jid", ghostJID) - return nil -} - -// CreateUser creates a ghost user via XEP-0077 registration -func (m *UserManager) CreateUser(user model.BridgeUser) error { - if err := user.Validate(); err != nil { - return fmt.Errorf("invalid user: %w", err) - } - - mattermostUserID := user.GetID() - - // Check if user already exists in KV store - if m.HasUser(mattermostUserID) { - return fmt.Errorf("user %s already exists", mattermostUserID) - } - - m.logger.LogDebug("Creating XMPP ghost user", "bridge_type", m.bridgeType, "mattermost_user_id", mattermostUserID, "display_name", user.GetDisplayName()) - - // Generate ghost user JID and password - ghostJID := m.generateGhostUserJID(mattermostUserID) - ghostPassword := generateSecurePassword() - - // Register ghost user via XEP-0077 - if err := m.registerGhostUser(mattermostUserID, ghostJID, ghostPassword); err != nil { - return fmt.Errorf("failed to register ghost user %s: %w", mattermostUserID, err) - } - - // Store ghost user data - ghostData := &GhostUserData{ - MattermostUserID: mattermostUserID, - GhostJID: ghostJID, - GhostPassword: ghostPassword, - Created: m.getCurrentTimestamp(), - } - - if err := m.storeGhostUserData(mattermostUserID, ghostData); err != nil { - return fmt.Errorf("failed to store ghost user data for %s: %w", mattermostUserID, err) - } - - m.logger.LogInfo("XMPP ghost user created successfully", "bridge_type", m.bridgeType, "mattermost_user_id", mattermostUserID) - return nil -} - -// GetUser retrieves a user by Mattermost user ID, creating XMPPUser from ghost data -func (m *UserManager) GetUser(mattermostUserID string) (model.BridgeUser, error) { - // Check if ghost user data exists - ghostData, err := m.loadGhostUserData(mattermostUserID) - if err != nil { - return nil, fmt.Errorf("ghost user not found for Mattermost user %s: %w", mattermostUserID, err) - } - - // Create XMPPUser directly with ghost credentials - m.configMu.RLock() - cfg := m.config - m.configMu.RUnlock() - - user := NewXMPPUser(mattermostUserID, mattermostUserID, ghostData.GhostJID, ghostData.GhostPassword, cfg, m.logger) - - // Ensure the user is connected - if err := m.ensureUserConnected(user, mattermostUserID); err != nil { - return nil, fmt.Errorf("failed to ensure ghost user is connected: %w", err) - } - - return user, nil -} - -// GetOrCreateUser retrieves a user by Mattermost user ID, creating a new ghost user if it doesn't exist -func (m *UserManager) GetOrCreateUser(mattermostUserID, displayName string) (model.BridgeUser, error) { - // Try to get existing user first - user, err := m.GetUser(mattermostUserID) - if err == nil { - return user, nil - } - - m.logger.LogDebug("Ghost user not found, creating new ghost user", "mattermost_user_id", mattermostUserID, "display_name", displayName) - - // User doesn't exist, create a new ghost user - m.configMu.RLock() - cfg := m.config - m.configMu.RUnlock() - - // Generate ghost user JID and password - ghostJID := m.generateGhostUserJID(mattermostUserID) - ghostPassword := generateSecurePassword() - - // Register ghost user via XEP-0077 first - if err := m.registerGhostUser(mattermostUserID, ghostJID, ghostPassword); err != nil { - return nil, fmt.Errorf("failed to register ghost user: %w", err) - } - - // Create XMPPUser instance with the correct ghost credentials - xmppUser := NewXMPPUser(mattermostUserID, displayName, ghostJID, ghostPassword, cfg, m.logger) - - // Store ghost user data - ghostData := &GhostUserData{ - MattermostUserID: mattermostUserID, - GhostJID: ghostJID, - GhostPassword: ghostPassword, - Created: m.getCurrentTimestamp(), - } - - if err := m.storeGhostUserData(mattermostUserID, ghostData); err != nil { - m.logger.LogWarn("Failed to store ghost user data", "mattermost_user_id", mattermostUserID, "jid", ghostJID, "error", err) - // Don't fail creation for storage issues, just log warning - } - - // Ensure the newly created user is connected - if err := m.ensureUserConnected(xmppUser, mattermostUserID); err != nil { - return nil, fmt.Errorf("failed to connect newly created ghost user: %w", err) - } - - m.logger.LogInfo("Ghost user created and connected successfully", "mattermost_user_id", mattermostUserID, "ghost_jid", ghostJID) - return xmppUser, nil -} - -// DeleteUser removes a user and cleans up ghost user account if cleanup is enabled -func (m *UserManager) DeleteUser(mattermostUserID string) error { - m.logger.LogDebug("Deleting XMPP ghost user", "bridge_type", m.bridgeType, "mattermost_user_id", mattermostUserID) - - // Check if ghost user data exists - if !m.HasUser(mattermostUserID) { - return fmt.Errorf("ghost user not found for Mattermost user %s", mattermostUserID) - } - - // Clean up ghost user account if cleanup is enabled - m.configMu.RLock() - shouldCleanup := m.config.IsGhostUserCleanupEnabled() - m.configMu.RUnlock() - - if shouldCleanup { - // Full cleanup: removes XMPP account + local data - if err := m.cleanupGhostUser(mattermostUserID); err != nil { - m.logger.LogWarn("Failed to cleanup ghost user account", "mattermost_user_id", mattermostUserID, "error", err) - // Don't fail deletion for cleanup issues, continue with local removal - } - } else { - // Only remove our local data, preserve XMPP account - if err := m.removeGhostUserData(mattermostUserID); err != nil { - m.logger.LogWarn("Failed to remove ghost user data from KV store", "mattermost_user_id", mattermostUserID, "error", err) - } - } - - m.logger.LogInfo("XMPP ghost user deleted successfully", "bridge_type", m.bridgeType, "mattermost_user_id", mattermostUserID) - return nil -} - -// ListUsers returns a list of all users from KV store -func (m *UserManager) ListUsers() []model.BridgeUser { - keys, err := m.kvstore.ListKeysWithPrefix(0, 100, ghostCredPrefix) - if err != nil { - m.logger.LogWarn("Failed to list ghost user keys from KV store", "error", err) - return []model.BridgeUser{} - } - - users := make([]model.BridgeUser, 0, len(keys)) - for _, key := range keys { - mattermostUserID := key[len(ghostCredPrefix):] - user, err := m.GetUser(mattermostUserID) - if err != nil { - m.logger.LogWarn("Failed to load user", "mattermost_user_id", mattermostUserID, "error", err) - continue - } - users = append(users, user) - } - - return users -} - -// HasUser checks if a ghost user exists for the given Mattermost user ID -func (m *UserManager) HasUser(mattermostUserID string) bool { - key := buildGhostUserKey(mattermostUserID) - value, err := m.kvstore.Get(key) - return err == nil && !bytes.Equal(value, []byte{}) // Check if key exists and is not empty -} - -// Start initializes the user manager and starts all ghost users from KV store -func (m *UserManager) Start(ctx context.Context) error { - m.logger.LogDebug("Starting XMPP ghost user manager", "bridge_type", m.bridgeType) - - m.ctx = ctx - - // Get all users from KV store and start them - users := m.ListUsers() - startedCount := 0 - - for _, user := range users { - if err := user.Start(ctx); err != nil { - m.logger.LogWarn("Failed to start ghost user during manager startup", "bridge_type", m.bridgeType, "user_id", user.GetID(), "error", err) - // Continue starting other users even if one fails - } else { - startedCount++ - } - } - - m.logger.LogInfo("XMPP ghost user manager started", "bridge_type", m.bridgeType, "user_count", startedCount) - return nil -} - -// Stop shuts down the user manager and all ghost users -func (m *UserManager) Stop() error { - m.logger.LogDebug("Stopping XMPP ghost user manager", "bridge_type", m.bridgeType) - - if m.cancel != nil { - m.cancel() - } - - // Get all users from KV store and stop them - users := m.ListUsers() - for _, user := range users { - if err := user.Stop(); err != nil { - m.logger.LogWarn("Error stopping ghost user during manager shutdown", "bridge_type", m.bridgeType, "user_id", user.GetID(), "error", err) - } - } - - m.logger.LogInfo("XMPP ghost user manager stopped", "bridge_type", m.bridgeType) - return nil -} - -// UpdateConfiguration updates configuration -func (m *UserManager) UpdateConfiguration(cfg *config.Configuration) error { - m.logger.LogDebug("Updating configuration for XMPP ghost user manager", "bridge_type", m.bridgeType) - - m.configMu.Lock() - m.config = cfg - m.configMu.Unlock() - - m.logger.LogInfo("XMPP ghost user manager configuration updated", "bridge_type", m.bridgeType) - return nil -} - -// GetBridgeType returns the bridge type this manager handles -func (m *UserManager) GetBridgeType() string { - return m.bridgeType -} - -// KV store operations for ghost user data only - -func (m *UserManager) storeGhostUserData(mattermostUserID string, data *GhostUserData) error { - key := buildGhostUserKey(mattermostUserID) - jsonData, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("failed to marshal ghost user data: %w", err) - } - return m.kvstore.Set(key, jsonData) -} - -func (m *UserManager) loadGhostUserData(mattermostUserID string) (*GhostUserData, error) { - key := buildGhostUserKey(mattermostUserID) - data, err := m.kvstore.Get(key) - if err != nil { - return nil, fmt.Errorf("ghost user data not found: %w", err) - } - - var ghostData GhostUserData - if err := json.Unmarshal(data, &ghostData); err != nil { - return nil, fmt.Errorf("failed to unmarshal ghost user data: %w", err) - } - - return &ghostData, nil -} - -func (m *UserManager) removeGhostUserData(mattermostUserID string) error { - key := buildGhostUserKey(mattermostUserID) - return m.kvstore.Delete(key) -} - -func (m *UserManager) cleanupGhostUser(mattermostUserID string) error { - ghostData, err := m.loadGhostUserData(mattermostUserID) - if err != nil { - // No ghost data found, nothing to cleanup - return nil - } - - // Get In-Band Registration handler from bridge client - regHandler, err := m.bridgeClient.GetInBandRegistration() - if err != nil { - return fmt.Errorf("failed to get in-band registration handler for cleanup: %w", err) - } - - // Parse the ghost JID to get the server part - ghostJIDParsed, err := jid.Parse(ghostData.GhostJID) - if err != nil { - return fmt.Errorf("failed to parse ghost JID %s for cleanup: %w", ghostData.GhostJID, err) - } - - // Unregister the ghost user account via XEP-0077 - response, err := regHandler.CancelRegistration(ghostJIDParsed.Domain()) - if err != nil { - return fmt.Errorf("failed to cancel registration for ghost user %s: %w", ghostData.GhostJID, err) - } - - if !response.Success { - m.logger.LogWarn("XEP-0077 registration cancellation failed", "jid", ghostData.GhostJID, "error", response.Error) - // Continue with cleanup even if cancellation failed - } - - // Remove ghost user data from KV store - if err := m.removeGhostUserData(mattermostUserID); err != nil { - m.logger.LogWarn("Failed to delete ghost user data from KV store", "mattermost_user_id", mattermostUserID, "error", err) - } - - m.logger.LogInfo("Ghost user account cleaned up successfully", "mattermost_user_id", mattermostUserID, "jid", ghostData.GhostJID) - return nil -} - -// ensureUserConnected ensures that a ghost user is connected to XMPP -func (m *UserManager) ensureUserConnected(xmppUser *User, mattermostUserID string) error { - // Check if user is already connected - if xmppUser.IsConnected() { - m.logger.LogDebug("Ghost user already connected", "mattermost_user_id", mattermostUserID, "ghost_jid", xmppUser.GetJID()) - return nil - } - - m.logger.LogDebug("Starting ghost user connection", "mattermost_user_id", mattermostUserID, "ghost_jid", xmppUser.GetJID()) - - // Start the user (this will connect it to XMPP) - if err := xmppUser.Start(m.ctx); err != nil { - return fmt.Errorf("failed to start ghost user %s: %w", xmppUser.GetJID(), err) - } - - // Verify connection was successful - if !xmppUser.IsConnected() { - return fmt.Errorf("ghost user %s failed to connect after start", xmppUser.GetJID()) - } - - m.logger.LogInfo("Ghost user connected successfully", "mattermost_user_id", mattermostUserID, "ghost_jid", xmppUser.GetJID()) - return nil -} - -// Helper functions - -func generateSecurePassword() string { - // TODO: Implement secure password generation using crypto/rand - return "temp_secure_password_123" -} - -func (m *UserManager) getCurrentTimestamp() int64 { - // TODO: Use proper time source (time.Now().Unix()) - return 0 -} diff --git a/server/config/config.go b/server/config/config.go index 9d651a6..db8c497 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -5,6 +5,8 @@ import ( "strings" ) +const DefaultXMPPUsernamePrefix = "xmpp" + // Configuration captures the plugin's external configuration as exposed in the Mattermost server // configuration, as well as values computed from the configuration. Any public fields will be // deserialized from the Mattermost server configuration in OnConfigurationChange. @@ -21,14 +23,9 @@ type Configuration struct { XMPPUsername string `json:"XMPPUsername"` XMPPPassword string `json:"XMPPPassword"` EnableSync bool `json:"EnableSync"` + XMPPUsernamePrefix string `json:"XMPPUsernamePrefix"` XMPPResource string `json:"XMPPResource"` XMPPInsecureSkipVerify bool `json:"XMPPInsecureSkipVerify"` - - // Ghost User Settings (XEP-0077 In-Band Registration) - EnableXMPPGhostUsers bool `json:"EnableXMPPGhostUsers"` - XMPPGhostUserPrefix string `json:"XMPPGhostUserPrefix"` - XMPPGhostUserDomain string `json:"XMPPGhostUserDomain"` - XMPPGhostUserCleanup bool `json:"XMPPGhostUserCleanup"` } // Equals compares two configuration structs @@ -44,12 +41,9 @@ func (c *Configuration) Equals(other *Configuration) bool { c.XMPPUsername == other.XMPPUsername && c.XMPPPassword == other.XMPPPassword && c.EnableSync == other.EnableSync && + c.XMPPUsernamePrefix == other.XMPPUsernamePrefix && c.XMPPResource == other.XMPPResource && - c.XMPPInsecureSkipVerify == other.XMPPInsecureSkipVerify && - c.EnableXMPPGhostUsers == other.EnableXMPPGhostUsers && - c.XMPPGhostUserPrefix == other.XMPPGhostUserPrefix && - c.XMPPGhostUserDomain == other.XMPPGhostUserDomain && - c.XMPPGhostUserCleanup == other.XMPPGhostUserCleanup + c.XMPPInsecureSkipVerify == other.XMPPInsecureSkipVerify } // Clone shallow copies the configuration. Your implementation may require a deep copy if @@ -59,6 +53,14 @@ func (c *Configuration) Clone() *Configuration { return &clone } +// GetXMPPUsernamePrefix returns the configured username prefix, or the default if not set +func (c *Configuration) GetXMPPUsernamePrefix() string { + if c.XMPPUsernamePrefix == "" { + return DefaultXMPPUsernamePrefix + } + return c.XMPPUsernamePrefix +} + // GetXMPPResource returns the configured XMPP resource, or a default if not set func (c *Configuration) GetXMPPResource() string { if c.XMPPResource == "" { @@ -67,41 +69,6 @@ func (c *Configuration) GetXMPPResource() string { return c.XMPPResource } -// GetXMPPGhostUserDomain returns the configured ghost user domain, or derives it from server URL -func (c *Configuration) GetXMPPGhostUserDomain() string { - if c.XMPPGhostUserDomain != "" { - return c.XMPPGhostUserDomain - } - - // Extract domain from bridge username as fallback - if c.XMPPUsername != "" && strings.Contains(c.XMPPUsername, "@") { - parts := strings.Split(c.XMPPUsername, "@") - if len(parts) > 1 { - return parts[1] - } - } - - // Last resort: try to extract from server URL - if c.XMPPServerURL != "" { - parts := strings.Split(c.XMPPServerURL, ":") - if len(parts) > 0 { - return parts[0] - } - } - - return "localhost" -} - -// IsGhostUserEnabled returns true if ghost users should be created -func (c *Configuration) IsGhostUserEnabled() bool { - return c.EnableXMPPGhostUsers && c.EnableSync -} - -// IsGhostUserCleanupEnabled returns true if ghost users should be cleaned up on removal -func (c *Configuration) IsGhostUserCleanupEnabled() bool { - return c.XMPPGhostUserCleanup -} - // IsValid validates the configuration and returns an error if invalid func (c *Configuration) IsValid() error { if c.EnableSync { @@ -120,16 +87,10 @@ func (c *Configuration) IsValid() error { return fmt.Errorf("XMPP Server URL must include port (e.g., server.com:5222)") } - // Validate ghost user configuration if enabled - if c.EnableXMPPGhostUsers { - if c.XMPPGhostUserPrefix == "" { - return fmt.Errorf("XMPP Ghost User Prefix is required when ghost users are enabled") - } - - // Validate ghost user prefix doesn't contain invalid characters - if strings.ContainsAny(c.XMPPGhostUserPrefix, ":@/\\") { - return fmt.Errorf("XMPP Ghost User Prefix cannot contain special characters (:, @, /, \\)") - } + // Validate username prefix doesn't contain invalid characters + prefix := c.GetXMPPUsernamePrefix() + if strings.ContainsAny(prefix, ":@/\\") { + return fmt.Errorf("XMPP Username Prefix cannot contain special characters (:, @, /, \\)") } } diff --git a/server/hooks_user.go b/server/hooks_user.go deleted file mode 100644 index 78c13a5..0000000 --- a/server/hooks_user.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/plugin" -) - -// UserHasBeenDeleted is called when a user has been deleted from Mattermost -// This allows us to clean up ghost users from the XMPP server -func (p *Plugin) UserHasBeenDeleted(c *plugin.Context, user *model.User) { - if user == nil { - p.logger.LogWarn("UserHasBeenDeleted called with nil user") - return - } - - p.logger.LogDebug("User deleted from Mattermost, cleaning up bridge users", "user_id", user.Id, "username", user.Username) - - // Clean up ghost users from external bridges (skip Mattermost bridge) - for _, bridgeName := range p.bridgeManager.ListBridges() { - // Skip the Mattermost bridge since it represents the Mattermost side - if bridgeName == "mattermost" { - p.logger.LogDebug("Skipping Mattermost bridge for user cleanup", "user_id", user.Id) - continue - } - - bridge, err := p.bridgeManager.GetBridge(bridgeName) - if err != nil { - p.logger.LogWarn("Failed to get bridge for user cleanup", "bridge", bridgeName, "error", err) - continue - } - - userManager := bridge.GetUserManager() - if userManager == nil { - p.logger.LogDebug("Bridge has no user manager, skipping cleanup", "bridge", bridgeName) - continue - } - - // Check if this user exists in the bridge - if !userManager.HasUser(user.Id) { - p.logger.LogDebug("User not found in bridge, skipping cleanup", "bridge", bridgeName, "user_id", user.Id) - continue - } - - // Delete the user from the bridge (this will handle ghost user cleanup if enabled) - if err := userManager.DeleteUser(user.Id); err != nil { - p.logger.LogWarn("Failed to delete user from bridge", "bridge", bridgeName, "user_id", user.Id, "error", err) - } else { - p.logger.LogInfo("Successfully deleted user from bridge", "bridge", bridgeName, "user_id", user.Id, "username", user.Username) - } - } -} diff --git a/server/xmpp/client.go b/server/xmpp/client.go index d7cdc37..98907c5 100644 --- a/server/xmpp/client.go +++ b/server/xmpp/client.go @@ -182,15 +182,11 @@ func (c *Client) GetInBandRegistration() (*InBandRegistration, error) { return c.XEPFeatures.InBandRegistration, nil } -// DetectServerCapabilities discovers which XEPs are supported by the server (public method) -func (c *Client) DetectServerCapabilities() error { - return c.detectServerCapabilities() -} - // detectServerCapabilities discovers which XEPs are supported by the server -func (c *Client) detectServerCapabilities() error { +func (c *Client) detectServerCapabilities() { if c.session == nil { - return fmt.Errorf("no XMPP session available for capability detection") + c.logger.LogError("Cannot detect server capabilities: no session") + return } c.logger.LogDebug("Detecting server capabilities for XEP support") @@ -207,7 +203,6 @@ func (c *Client) detectServerCapabilities() error { enabledFeatures := c.XEPFeatures.ListFeatures() c.logger.LogInfo("Server capability detection completed", "enabled_xeps", enabledFeatures) - return nil } // checkInBandRegistrationSupport checks if the server supports XEP-0077 In-Band Registration @@ -360,6 +355,10 @@ func (c *Client) Connect() error { return fmt.Errorf("failed to start session serving") } c.logger.LogInfo("XMPP client connected successfully", "jid", c.jidAddr.String()) + + // Detect server capabilities and enable supported XEPs + go c.detectServerCapabilities() + return nil case <-time.After(10 * time.Second): return fmt.Errorf("timeout waiting for session to be ready") @@ -694,51 +693,6 @@ func (c *Client) SetOfflinePresence() error { return nil } -// CheckRoomMembership verifies if the client is joined to an XMPP room -// Since disco#items is not available in this XMPP library, we use a simpler approach: -// we assume the user is not joined and always attempt to join (XMPP MUC handles duplicate joins gracefully) -func (c *Client) CheckRoomMembership(roomJID string) (bool, error) { - if c.session == nil { - return false, fmt.Errorf("XMPP session not established") - } - - c.logger.LogDebug("Checking room membership (conservative approach - assume not joined)", "room_jid", roomJID) - - // For safety, we always return false to ensure EnsureJoinedToRoom will attempt to join - // XMPP MUC servers handle duplicate joins gracefully by ignoring them - return false, nil -} - -// EnsureJoinedToRoom ensures the client is joined to an XMPP room, joining if necessary -func (c *Client) EnsureJoinedToRoom(roomJID string) error { - if c.session == nil { - return fmt.Errorf("XMPP session not established") - } - - c.logger.LogDebug("Ensuring joined to room", "room_jid", roomJID) - - // First check if we're already joined - isJoined, err := c.CheckRoomMembership(roomJID) - if err != nil { - c.logger.LogWarn("Failed to check room membership, attempting to join anyway", "room_jid", roomJID, "error", err) - // Continue with join attempt even if membership check failed - } else if isJoined { - c.logger.LogDebug("Already joined to room", "room_jid", roomJID) - return nil - } - - // Not joined, attempt to join the room - c.logger.LogDebug("Not joined to room, attempting to join", "room_jid", roomJID) - - err = c.JoinRoom(roomJID) - if err != nil { - return fmt.Errorf("failed to join room %s: %w", roomJID, err) - } - - c.logger.LogInfo("Successfully joined room", "room_jid", roomJID) - return nil -} - // CheckRoomExists verifies if an XMPP room exists and is accessible using disco#info func (c *Client) CheckRoomExists(roomJID string) (bool, error) { if c.session == nil { diff --git a/server/xmpp/xep_0077.go b/server/xmpp/xep_0077.go index 9716fd7..65ff8fa 100644 --- a/server/xmpp/xep_0077.go +++ b/server/xmpp/xep_0077.go @@ -73,10 +73,10 @@ type RegistrationResponse struct { } // NewInBandRegistration creates a new InBandRegistration XEP handler -func NewInBandRegistration(client *Client, log logger.Logger) *InBandRegistration { +func NewInBandRegistration(client *Client, logger logger.Logger) *InBandRegistration { return &InBandRegistration{ client: client, - logger: log, + logger: logger, enabled: true, // Default enabled } } diff --git a/server/xmpp/xep_features.go b/server/xmpp/xep_features.go index 75b923d..d50fd12 100644 --- a/server/xmpp/xep_features.go +++ b/server/xmpp/xep_features.go @@ -26,9 +26,9 @@ type XEPFeatures struct { } // NewXEPFeatures creates a new XEP features manager -func NewXEPFeatures(log logger.Logger) *XEPFeatures { +func NewXEPFeatures(logger logger.Logger) *XEPFeatures { return &XEPFeatures{ - logger: log, + logger: logger, } } diff --git a/sidecar/README.md b/sidecar/README.md index 51d2f33..5faf672 100644 --- a/sidecar/README.md +++ b/sidecar/README.md @@ -47,18 +47,18 @@ Open your web browser and go to: http://localhost:9090 ### 2. Complete Setup Wizard 1. **Language Selection**: Choose your preferred language -2. **Server Settings**: - - Server Domain and Server Host Name (FQDN): `localhost` +2. **Server Settings**: + - Server Domain: `localhost` (default is fine) - Keep other defaults -3. **Database Settings**: +3. **Database Settings**: - Choose "Embedded Database" for development - This creates a local database that persists in Docker volumes -4. **Profile Settings**: +4. **Profile Settings**: - Choose "Default" (no LDAP needed for development) 5. **Administrator Account**: - - Email: `admin@localhost` + - Username: `admin` - Password: `admin` (for development consistency) -6. When finishing setup the server will be non-responsive for a minute + - Email: `admin@localhost` ### 3. Create Test User @@ -83,8 +83,13 @@ For testing Multi-User Chat functionality, create a test room: - **Room ID**: `test1` - **Room Name**: `Test Room 1` - **Description**: `Test room for XMPP bridge development` -3. Leave rest as defaults. -4. Click **Save changes** + - **Subject**: `Development Test Room` +3. Configure room settings: + - **Room Type**: Public (searchable and accessible) + - **Persistent**: Yes (room survives server restarts) + - **Max occupants**: 50 (or leave default) + - **Enable**: Yes +4. Click **Create Room** The room will be accessible as `test1@conference.localhost` for testing MUC operations. @@ -164,4 +169,4 @@ go run cmd/xmpp-client-doctor/main.go \ - The server uses self-signed certificates, so the doctor tool defaults to `-insecure-skip-verify=true` - All data persists between container restarts unless you run `make devserver_clean` - The PostgreSQL and Adminer services are included but optional (you can use embedded database) -- The server takes ~30 seconds to fully start up after `docker compose up` +- The server takes ~30 seconds to fully start up after `docker compose up` \ No newline at end of file diff --git a/sidecar/docker-compose.yml b/sidecar/docker-compose.yml index b0f765b..f3177d2 100644 --- a/sidecar/docker-compose.yml +++ b/sidecar/docker-compose.yml @@ -1,5 +1,3 @@ -name: xmpp-bridge-openfire - services: openfire: image: nasqueron/openfire:4.7.1