feat: implement comprehensive loop prevention and architecture improvements

- Add comprehensive loop prevention at source level for all bridges:
  - XMPP bridge: Skip messages from own XMPP connection user
  - Mattermost bridge: Skip messages from bot user and remote users
- Remove cache from getOrCreateRemoteUser method for simplified user management
- Improve XMPP client architecture with direct handler delegation:
  - Add SetMessageHandler and GetJID methods to XMPP client
  - Move protocol normalization methods to client level
  - Implement handleIncomingXMPPMessage in XMPP bridge for business logic
- Fix message direction handling in XMPP message handler
- Add remote user invitation to shared channels via InviteRemoteToChannel API
- Clean up unused code and improve code formatting

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Felipe M 2025-08-06 17:16:52 +02:00
parent 11a32afc53
commit d9c0215b93
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
6 changed files with 63 additions and 44 deletions

View file

@ -278,7 +278,7 @@ func (m *BridgeManager) CreateChannelMapping(req model.CreateChannelMappingReque
return fmt.Errorf("bridge '%s' is not connected", req.BridgeName)
}
// NEW: Check if room already mapped to another channel
// Check if channel mapping already exists on the bridge
existingChannelID, err := bridge.GetChannelMapping(req.BridgeChannelID)
if err != nil {
m.logger.LogError("Failed to check channel mapping", "bridge_channel_id", req.BridgeChannelID, "error", err)

View file

@ -3,7 +3,6 @@ package mattermost
import (
"fmt"
"strings"
"sync"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
@ -14,8 +13,6 @@ import (
type mattermostMessageHandler struct {
bridge *mattermostBridge
logger logger.Logger
userCache map[string]string // Maps "bridgeType:remoteID:userID" -> Mattermost user ID
cacheMu sync.RWMutex // Protects userCache
}
// newMessageHandler creates a new Mattermost message handler
@ -23,7 +20,6 @@ func newMessageHandler(bridge *mattermostBridge) *mattermostMessageHandler {
return &mattermostMessageHandler{
bridge: bridge,
logger: bridge.logger,
userCache: make(map[string]string),
}
}
@ -91,6 +87,16 @@ func (h *mattermostMessageHandler) postMessageToMattermost(msg *pluginModel.Brid
return fmt.Errorf("failed to get or create remote user: %w", err)
}
if err := h.bridge.api.InviteRemoteToChannel(channelID, msg.SourceRemoteID, remoteUserID, true); err != nil {
h.logger.LogError("Failed to invite remote user to channel",
"channel_id", msg.SourceChannelID,
"remote_user_id", remoteUserID,
"source_bridge", msg.SourceBridge,
"source_remote_id", msg.SourceRemoteID,
"err", err.Error(),
)
}
// Create the post using the remote user (no need for bridge formatting since it's posted as the actual user)
post := &mmModel.Post{
ChannelId: channelID,
@ -128,26 +134,6 @@ func (h *mattermostMessageHandler) postMessageToMattermost(msg *pluginModel.Brid
// getOrCreateRemoteUser gets or creates a remote user for incoming bridge messages
func (h *mattermostMessageHandler) getOrCreateRemoteUser(msg *pluginModel.BridgeMessage) (string, error) {
// Create cache key: "bridgeType:remoteID:userID"
cacheKey := fmt.Sprintf("%s:%s:%s", msg.SourceBridge, msg.SourceRemoteID, msg.SourceUserID)
// Check cache first
h.cacheMu.RLock()
if userID, exists := h.userCache[cacheKey]; exists {
h.cacheMu.RUnlock()
return userID, nil
}
h.cacheMu.RUnlock()
// Lock for user creation
h.cacheMu.Lock()
defer h.cacheMu.Unlock()
// Double-check cache after acquiring lock
if userID, exists := h.userCache[cacheKey]; exists {
return userID, nil
}
// Generate username from source info
username := h.generateUsername(msg.SourceUserID, msg.SourceUserName, msg.SourceBridge)
@ -158,7 +144,6 @@ func (h *mattermostMessageHandler) getOrCreateRemoteUser(msg *pluginModel.Bridge
if existingUser, appErr := h.bridge.api.GetUserByUsername(username); appErr == nil && existingUser != nil {
// Check if this user has the correct RemoteId
if existingUser.RemoteId != nil && *existingUser.RemoteId == msg.SourceRemoteID {
h.userCache[cacheKey] = existingUser.Id
h.logger.LogDebug("Found existing remote user",
"user_id", existingUser.Id,
"username", username,
@ -172,7 +157,6 @@ func (h *mattermostMessageHandler) getOrCreateRemoteUser(msg *pluginModel.Bridge
if existingUser, appErr := h.bridge.api.GetUserByEmail(email); appErr == nil && existingUser != nil {
// Check if this user has the correct RemoteId
if existingUser.RemoteId != nil && *existingUser.RemoteId == msg.SourceRemoteID {
h.userCache[cacheKey] = existingUser.Id
h.logger.LogDebug("Found existing remote user by email",
"user_id", existingUser.Id,
"email", email,
@ -188,7 +172,7 @@ func (h *mattermostMessageHandler) getOrCreateRemoteUser(msg *pluginModel.Bridge
Email: email,
FirstName: msg.SourceUserName,
Password: mmModel.NewId(),
RemoteId: &msg.SourceRemoteID,
RemoteId: mmModel.NewPointer(msg.SourceRemoteID),
}
// Try to create the user
@ -203,9 +187,6 @@ func (h *mattermostMessageHandler) getOrCreateRemoteUser(msg *pluginModel.Bridge
return "", fmt.Errorf("failed to create remote user: %w", appErr)
}
// Cache the result
h.userCache[cacheKey] = createdUser.Id
h.logger.LogInfo("Created remote user",
"user_id", createdUser.Id,
"username", username,

View file

@ -181,7 +181,6 @@ func (b *xmppBridge) Start() error {
// Start connection monitor
go b.connectionMonitor()
b.logger.LogInfo("Mattermost to XMPP bridge started successfully")
return nil
}
@ -574,7 +573,6 @@ func (b *xmppBridge) GetUserManager() pluginModel.BridgeUserManager {
return b.userManager
}
// GetMessageChannel returns the channel for incoming messages from XMPP
func (b *xmppBridge) GetMessageChannel() <-chan *pluginModel.DirectionalMessage {
return b.incomingMessages
@ -638,6 +636,14 @@ func (b *xmppBridge) handleIncomingXMPPMessage(msg stanza.Message, t xmlstream.T
userID, displayName := b.bridgeClient.ExtractUserInfo(msg.From)
// 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
}
// Create bridge message
bridgeMessage := &pluginModel.BridgeMessage{
SourceBridge: b.bridgeID,

View file

@ -37,7 +37,7 @@ func (h *xmppMessageHandler) ProcessMessage(msg *pluginModel.DirectionalMessage)
}
// For incoming messages to XMPP, we send them to XMPP rooms
if msg.Direction == pluginModel.DirectionIncoming {
if msg.Direction == pluginModel.DirectionOutgoing {
return h.sendMessageToXMPP(msg.BridgeMessage)
}

View file

@ -53,6 +53,8 @@ func (p *Plugin) OnSharedChannelsPing(remoteCluster *model.RemoteCluster) bool {
// OnSharedChannelsSyncMsg processes sync messages from Mattermost shared channels and routes them to XMPP
func (p *Plugin) OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error) {
p.logger.LogDebug("🚀 OnSharedChannelsSyncMsg called", "remote_id", rc.RemoteId, "channel_id", msg.ChannelId)
config := p.getConfiguration()
// Initialize sync response
@ -109,6 +111,30 @@ func (p *Plugin) OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteClu
// processSyncPost converts a Mattermost post to a bridge message and routes it to XMPP
func (p *Plugin) processSyncPost(post *model.Post, channelID string, users map[string]*model.User) error {
p.logger.LogDebug("Processing sync post", "post_id", post.Id, "channel_id", channelID, "users", users)
// Skip messages from our own bot user to prevent loops
if post.UserId == p.botUserID {
p.logger.LogDebug("Skipping message from bot user to prevent loop",
"bot_user_id", p.botUserID,
"post_user_id", post.UserId)
return nil
}
// Skip messages from remote users to prevent loops
// Remote users represent users from other bridges (e.g., XMPP users in Mattermost)
user, appErr := p.API.GetUser(post.UserId)
if appErr != nil {
p.logger.LogWarn("Failed to get user details for loop prevention. Ignoring message.", "user_id", post.UserId, "error", appErr)
return nil
} else if user != nil && user.RemoteId != nil && *user.RemoteId != "" {
p.logger.LogDebug("Skipping message from remote user to prevent loop",
"user_id", post.UserId,
"username", user.Username,
"remote_id", *user.RemoteId)
return nil
}
// Find the user who created this post
var postUser *model.User
p.logger.LogInfo("Processing sync post", "post_id", post.UserId, "users", users)
@ -118,10 +144,10 @@ func (p *Plugin) processSyncPost(post *model.Post, channelID string, users map[s
// If user not found in sync data, try to get from API
if postUser == nil {
var err error
postUser, err = p.API.GetUser(post.UserId)
if err != nil {
p.logger.LogWarn("Failed to get user for post", "user_id", post.UserId, "post_id", post.Id, "error", err)
var appErr *model.AppError
postUser, appErr = p.API.GetUser(post.UserId)
if appErr != nil {
p.logger.LogWarn("Failed to get user for post", "user_id", post.UserId, "post_id", post.Id, "error", appErr)
// Create a placeholder user
postUser = &model.User{
Id: post.UserId,
@ -136,6 +162,7 @@ func (p *Plugin) processSyncPost(post *model.Post, channelID string, users map[s
SourceChannelID: channelID,
SourceUserID: postUser.Id,
SourceUserName: postUser.Username,
SourceRemoteID: "", // This message comes from Mattermost, so no remote ID
Content: post.Message,
MessageType: "text", // TODO: Handle other message types
Timestamp: time.Unix(post.CreateAt/1000, 0),

View file

@ -161,6 +161,11 @@ func (c *Client) SetMessageHandler(handler mux.MessageHandlerFunc) {
c.messageHandler = handler
}
// GetJID returns the client's JID
func (c *Client) GetJID() jid.JID {
return c.jidAddr
}
// parseServerAddress parses a server URL and returns a host:port address
func (c *Client) parseServerAddress(serverURL string) (string, error) {
// Handle simple host:port format (e.g., "localhost:5222")