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:
parent
11a32afc53
commit
d9c0215b93
6 changed files with 63 additions and 44 deletions
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
@ -12,18 +11,15 @@ import (
|
|||
|
||||
// mattermostMessageHandler handles incoming messages for the Mattermost bridge
|
||||
type mattermostMessageHandler struct {
|
||||
bridge *mattermostBridge
|
||||
logger logger.Logger
|
||||
userCache map[string]string // Maps "bridgeType:remoteID:userID" -> Mattermost user ID
|
||||
cacheMu sync.RWMutex // Protects userCache
|
||||
bridge *mattermostBridge
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// newMessageHandler creates a new Mattermost message handler
|
||||
func newMessageHandler(bridge *mattermostBridge) *mattermostMessageHandler {
|
||||
return &mattermostMessageHandler{
|
||||
bridge: bridge,
|
||||
logger: bridge.logger,
|
||||
userCache: make(map[string]string),
|
||||
bridge: bridge,
|
||||
logger: bridge.logger,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -32,8 +32,8 @@ type xmppBridge struct {
|
|||
kvstore kvstore.KVStore
|
||||
bridgeClient *xmppClient.Client // Main bridge XMPP client connection
|
||||
userManager pluginModel.BridgeUserManager
|
||||
bridgeID string // Bridge identifier used for registration
|
||||
remoteID string // Remote ID for shared channels
|
||||
bridgeID string // Bridge identifier used for registration
|
||||
remoteID string // Remote ID for shared channels
|
||||
|
||||
// Message handling
|
||||
messageHandler *xmppMessageHandler
|
||||
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue