mattermost-plugin-bridge-xmpp/server/bridge/xmpp/user_manager.go
Felipe Martin 0442bd7b72
Some checks failed
ci / plugin-ci (push) Has been cancelled
feat: implement connection caching for ghost user lifecycle management
Implement comprehensive connection caching system to prevent XMPP connection leaks and support HA environments:

- Add node-specific XMPP resources using format "{baseResource}-node-{diagnosticId[:8]}" for HA compatibility
- Implement thread-safe connection cache with mutex protection in UserManager
- Add cache-first lookup in GetUser/GetOrCreateUser methods to prevent duplicate connections
- Update lifecycle manager to efficiently check cached users instead of expensive KV store queries
- Add graceful shutdown cleanup to properly disconnect all cached connections
- Implement cache management methods: getCachedUser, cacheUser, removeCachedUser, getCachedUsers
- Update activity tracking to work with cached connections
- Add proper cache cleanup when users are disconnected or deleted

This prevents connection leaks identified in previous implementation while maintaining efficient ghost user lifecycle management with 30-minute inactivity timeout.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-12 21:18:35 +02:00

734 lines
26 KiB
Go

package xmpp
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"sync"
"time"
"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
LastActivity int64 `json:"last_activity"` // Unix timestamp of last activity
LifecycleEnabled bool `json:"lifecycle_enabled"` // Whether this user should be subject to inactivity checking
}
// 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
// Node identification for HA environments
nodeID string // Unique identifier for this Mattermost node (from api.GetDiagnosticId)
// Connection caching to prevent connection leaks
activeUsers map[string]*User // Cache of connected users on THIS node
activeUsersMu sync.RWMutex // Protects activeUsers map
}
// 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())
// Get unique node ID from Mattermost API for HA environments
nodeID := api.GetDiagnosticId()
log.LogDebug("Initializing XMPP user manager", "bridge_type", bridgeType, "node_id", nodeID[:8])
return &UserManager{
bridgeType: bridgeType,
logger: log,
kvstore: store,
api: api,
config: cfg,
bridgeClient: bridgeClient,
ctx: ctx,
cancel: cancel,
nodeID: nodeID,
activeUsers: make(map[string]*User), // Initialize connection cache
}
}
// 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, checking cache first, then creating XMPPUser from ghost data
func (m *UserManager) GetUser(mattermostUserID string) (model.BridgeUser, error) {
// First check the connection cache
if cachedUser, found := m.getCachedUser(mattermostUserID); found {
m.logger.LogDebug("Found user in connection cache", "user_id", mattermostUserID, "ghost_jid", cachedUser.GetJID())
return cachedUser, nil
}
// 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 and activity data
m.configMu.RLock()
cfg := m.config
m.configMu.RUnlock()
// Handle migration of existing users without activity data
lastActivity := time.Now()
if ghostData.LastActivity > 0 {
lastActivity = time.Unix(ghostData.LastActivity, 0)
} else {
// Update the KV store with the migration data
ghostData.LastActivity = lastActivity.Unix()
ghostData.LifecycleEnabled = true
_ = m.storeGhostUserData(mattermostUserID, ghostData) // Don't fail if storage update fails
}
user := m.createXMPPUserWithActivity(mattermostUserID, mattermostUserID, ghostData.GhostJID, ghostData.GhostPassword, cfg, m.logger, lastActivity, ghostData.LifecycleEnabled)
// 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)
}
// Cache the connected user to prevent connection leaks
m.cacheUser(mattermostUserID, user)
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) {
// First check the connection cache
if cachedUser, found := m.getCachedUser(mattermostUserID); found {
m.logger.LogDebug("Found user in connection cache", "user_id", mattermostUserID, "ghost_jid", cachedUser.GetJID())
return cachedUser, nil
}
// Try to get existing user from KV store
user, err := m.GetUser(mattermostUserID)
if err == nil {
// GetUser already cached the user, so just return it
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)
}
// Initialize activity data for new user
now := time.Now()
// Create XMPPUser instance with the correct ghost credentials and activity data
xmppUser := m.createXMPPUserWithActivity(mattermostUserID, displayName, ghostJID, ghostPassword, cfg, m.logger, now, true)
// Store ghost user data with activity tracking
ghostData := &GhostUserData{
MattermostUserID: mattermostUserID,
GhostJID: ghostJID,
GhostPassword: ghostPassword,
Created: now.Unix(),
LastActivity: now.Unix(),
LifecycleEnabled: true,
}
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)
}
// Cache the connected user to prevent connection leaks
m.cacheUser(mattermostUserID, xmppUser)
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)
}
// Disconnect and remove from cache if user is currently active
if cachedUser, found := m.getCachedUser(mattermostUserID); found {
if err := cachedUser.Disconnect(); err != nil {
m.logger.LogWarn("Failed to disconnect cached user during deletion", "mattermost_user_id", mattermostUserID, "error", err)
}
m.removeCachedUser(mattermostUserID)
m.logger.LogDebug("Disconnected and removed user from cache during deletion", "mattermost_user_id", 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++
// Enable lifecycle management for ghost users
if xmppUser, ok := user.(*User); ok {
xmppUser.SetLifecycleManagement(true)
}
}
}
// Start the lifecycle management goroutine
go m.lifecycleManager()
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)
// Cancel context to stop background goroutines
if m.cancel != nil {
m.cancel()
}
// Gracefully shutdown all cached connections first (much faster than ListUsers)
cachedUsers := m.getCachedUsers()
disconnectedCount := 0
for _, user := range cachedUsers {
if err := user.Stop(); err != nil {
m.logger.LogWarn("Error stopping cached ghost user during manager shutdown", "bridge_type", m.bridgeType, "user_id", user.GetID(), "error", err)
} else {
disconnectedCount++
}
}
// Clear the entire cache
m.activeUsersMu.Lock()
m.activeUsers = make(map[string]*User)
m.activeUsersMu.Unlock()
// Also check for any users not in cache and stop them (fallback)
users := m.ListUsers()
fallbackStoppedCount := 0
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)
} else {
fallbackStoppedCount++
}
}
m.logger.LogInfo("XMPP ghost user manager stopped",
"bridge_type", m.bridgeType,
"cached_users_stopped", disconnectedCount,
"fallback_users_stopped", fallbackStoppedCount)
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)
}
// UpdateUserActivity updates both the in-memory and persisted activity timestamp for a user
func (m *UserManager) UpdateUserActivity(mattermostUserID string) error {
// Load existing ghost user data
ghostData, err := m.loadGhostUserData(mattermostUserID)
if err != nil {
return fmt.Errorf("failed to load ghost user data for activity update: %w", err)
}
// Update the activity timestamp
now := time.Now()
ghostData.LastActivity = now.Unix()
// Store the updated data
if err := m.storeGhostUserData(mattermostUserID, ghostData); err != nil {
return fmt.Errorf("failed to persist activity update: %w", err)
}
m.logger.LogDebug("Updated user activity in KV store",
"user_id", mattermostUserID,
"timestamp", now)
return nil
}
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
cancellationRequest := &xmppClient.CancellationRequest{
Username: ghostJIDParsed.Localpart(), // Extract username from ghost JID
}
response, err := regHandler.CancelRegistration(ghostJIDParsed.Domain(), cancellationRequest)
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"
}
// lifecycleManager runs periodically to check for inactive ghost users and disconnect them
func (m *UserManager) lifecycleManager() {
m.logger.LogDebug("Starting lifecycle manager for ghost user cleanup")
ticker := time.NewTicker(ghostUserActivityCheckInterval)
defer ticker.Stop()
for {
select {
case <-m.ctx.Done():
m.logger.LogDebug("Lifecycle manager stopped due to context cancellation")
return
case <-ticker.C:
m.checkAndDisconnectInactiveUsers()
}
}
}
// checkAndDisconnectInactiveUsers checks all cached users for inactivity and disconnects inactive ghost users
func (m *UserManager) checkAndDisconnectInactiveUsers() {
m.logger.LogDebug("Checking cached users for inactivity")
// Get all currently cached users (this is much more efficient than ListUsers)
cachedUsers := m.getCachedUsers()
inactiveCount := 0
disconnectedCount := 0
for _, xmppUser := range cachedUsers {
// Only check users that have lifecycle management enabled
if !xmppUser.IsLifecycleManaged() {
continue
}
// Check if user is connected and inactive
if xmppUser.IsConnected() && xmppUser.IsInactive(ghostUserInactivityTimeout) {
inactiveCount++
lastActivity := xmppUser.GetLastActivity()
inactiveDuration := time.Since(lastActivity)
m.logger.LogInfo("Disconnecting inactive ghost user",
"user_id", xmppUser.GetID(),
"jid", xmppUser.GetJID(),
"last_activity", lastActivity,
"inactive_duration", inactiveDuration)
// Gracefully disconnect the inactive user
if err := xmppUser.Disconnect(); err != nil {
m.logger.LogWarn("Failed to disconnect inactive ghost user",
"user_id", xmppUser.GetID(),
"jid", xmppUser.GetJID(),
"error", err)
} else {
disconnectedCount++
// Remove disconnected user from cache to free memory
m.removeCachedUser(xmppUser.GetID())
m.logger.LogDebug("Successfully disconnected and removed inactive ghost user from cache",
"user_id", xmppUser.GetID(),
"jid", xmppUser.GetJID())
}
}
}
if inactiveCount > 0 {
m.logger.LogInfo("Completed inactive user cleanup cycle",
"cached_users_checked", len(cachedUsers),
"inactive_users_found", inactiveCount,
"users_disconnected", disconnectedCount)
} else {
m.logger.LogDebug("No inactive users found during cleanup cycle", "cached_users_checked", len(cachedUsers))
}
}
// createXMPPUserWithActivity creates an XMPP user with node-specific resource and activity data
func (m *UserManager) createXMPPUserWithActivity(id, displayName, userJID, password string, cfg *config.Configuration, log logger.Logger, lastActivity time.Time, enableLifecycle bool) *User {
// Generate node-specific resource to prevent conflicts in HA environments
baseResource := cfg.GetXMPPResource()
nodeSpecificResource := fmt.Sprintf("%s-node-%s", baseResource, m.nodeID[:8])
m.logger.LogDebug("Creating XMPP user with node-specific resource",
"user_id", id,
"base_resource", baseResource,
"node_resource", nodeSpecificResource,
"node_id", m.nodeID[:8])
// Create TLS config based on certificate verification setting
tlsConfig := &tls.Config{
InsecureSkipVerify: cfg.XMPPInsecureSkipVerify, //nolint:gosec // Allow insecure TLS for testing environments
}
// Create XMPP client for this user with provided credentials and node-specific resource
client := xmppClient.NewClientWithTLS(
cfg.XMPPServerURL,
userJID,
password, // Use the provided password (ghost password)
nodeSpecificResource, // Use node-specific resource instead of base resource
id, // Use user ID as remote ID
tlsConfig,
log,
)
ctx, cancel := context.WithCancel(context.Background())
return &User{
id: id,
displayName: displayName,
jid: userJID,
client: client,
state: model.UserStateOffline,
config: cfg,
ctx: ctx,
cancel: cancel,
logger: log,
lastActivity: lastActivity, // Use provided activity time
enableLifecycleCheck: enableLifecycle, // Use provided lifecycle setting
}
}
// Cache management methods for connection caching
// getCachedUser retrieves a user from the connection cache
func (m *UserManager) getCachedUser(mattermostUserID string) (*User, bool) {
m.activeUsersMu.RLock()
defer m.activeUsersMu.RUnlock()
user, exists := m.activeUsers[mattermostUserID]
return user, exists
}
// cacheUser stores a user in the connection cache
func (m *UserManager) cacheUser(mattermostUserID string, user *User) {
m.activeUsersMu.Lock()
defer m.activeUsersMu.Unlock()
m.activeUsers[mattermostUserID] = user
m.logger.LogDebug("Cached user connection",
"user_id", mattermostUserID,
"ghost_jid", user.GetJID(),
"cache_size", len(m.activeUsers))
}
// removeCachedUser removes a user from the connection cache
func (m *UserManager) removeCachedUser(mattermostUserID string) {
m.activeUsersMu.Lock()
defer m.activeUsersMu.Unlock()
if user, exists := m.activeUsers[mattermostUserID]; exists {
delete(m.activeUsers, mattermostUserID)
m.logger.LogDebug("Removed user from cache",
"user_id", mattermostUserID,
"ghost_jid", user.GetJID(),
"cache_size", len(m.activeUsers))
}
}
// getCachedUsers returns all cached users (for lifecycle management)
func (m *UserManager) getCachedUsers() []*User {
m.activeUsersMu.RLock()
defer m.activeUsersMu.RUnlock()
users := make([]*User, 0, len(m.activeUsers))
for _, user := range m.activeUsers {
users = append(users, user)
}
return users
}
func (m *UserManager) getCurrentTimestamp() int64 {
// TODO: Use proper time source (time.Now().Unix())
return 0
}