feat: implement remote user creation for Mattermost bridge

Replace bot-based message posting with remote user creation system:

- Add getOrCreateRemoteUser() method to create/retrieve remote users on-demand
- Use plugin API to find existing users by username/email before creating new ones
- Generate usernames with bridge prefix and emails with bridge.{bridgeID} domain
- Set RemoteId field to BridgeMessage.SourceRemoteID for proper loop prevention
- Cache user mappings to avoid repeated API calls
- Post messages directly as remote users instead of bot with metadata
- Remove unused message formatting since messages are posted as actual users
- Log errors for failed user creation without complex retry logic

This enables authentic user attribution in Mattermost channels while maintaining
existing loop prevention mechanisms through the RemoteId field.

🤖 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 12:29:55 +02:00
parent 245f5f96db
commit 8e9d87b176
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A

View file

@ -3,6 +3,7 @@ package mattermost
import ( import (
"fmt" "fmt"
"strings" "strings"
"sync"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger" "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model" pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
@ -11,15 +12,18 @@ import (
// mattermostMessageHandler handles incoming messages for the Mattermost bridge // mattermostMessageHandler handles incoming messages for the Mattermost bridge
type mattermostMessageHandler struct { type mattermostMessageHandler struct {
bridge *mattermostBridge bridge *mattermostBridge
logger logger.Logger 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 // newMessageHandler creates a new Mattermost message handler
func newMessageHandler(bridge *mattermostBridge) *mattermostMessageHandler { func newMessageHandler(bridge *mattermostBridge) *mattermostMessageHandler {
return &mattermostMessageHandler{ return &mattermostMessageHandler{
bridge: bridge, bridge: bridge,
logger: bridge.logger, logger: bridge.logger,
userCache: make(map[string]string),
} }
} }
@ -81,19 +85,20 @@ func (h *mattermostMessageHandler) postMessageToMattermost(msg *pluginModel.Brid
return fmt.Errorf("channel %s not found", channelID) return fmt.Errorf("channel %s not found", channelID)
} }
// Format the message content // Get or create remote user for this message
content := h.formatMessageContent(msg) remoteUserID, err := h.getOrCreateRemoteUser(msg)
if err != nil {
return fmt.Errorf("failed to get or create remote user: %w", err)
}
// Create the post // Create the post using the remote user (no need for bridge formatting since it's posted as the actual user)
post := &mmModel.Post{ post := &mmModel.Post{
ChannelId: channelID, ChannelId: channelID,
UserId: h.bridge.botUserID, UserId: remoteUserID,
Message: content, Message: msg.Content,
Type: mmModel.PostTypeDefault, Type: mmModel.PostTypeDefault,
Props: map[string]interface{}{ Props: map[string]interface{}{
"from_bridge": msg.SourceBridge, "from_bridge": msg.SourceBridge,
"bridge_user_id": msg.SourceUserID,
"bridge_user_name": msg.SourceUserName,
"bridge_message_id": msg.MessageID, "bridge_message_id": msg.MessageID,
"bridge_timestamp": msg.Timestamp.Unix(), "bridge_timestamp": msg.Timestamp.Unix(),
}, },
@ -113,34 +118,123 @@ func (h *mattermostMessageHandler) postMessageToMattermost(msg *pluginModel.Brid
h.logger.LogDebug("Message posted to Mattermost channel", h.logger.LogDebug("Message posted to Mattermost channel",
"channel_id", channelID, "channel_id", channelID,
"post_id", createdPost.Id, "post_id", createdPost.Id,
"remote_user_id", remoteUserID,
"source_bridge", msg.SourceBridge, "source_bridge", msg.SourceBridge,
"content_length", len(content)) "source_user", msg.SourceUserName,
"content_length", len(msg.Content))
return nil return nil
} }
// formatMessageContent formats the message content for Mattermost // getOrCreateRemoteUser gets or creates a remote user for incoming bridge messages
func (h *mattermostMessageHandler) formatMessageContent(msg *pluginModel.BridgeMessage) string { func (h *mattermostMessageHandler) getOrCreateRemoteUser(msg *pluginModel.BridgeMessage) (string, error) {
// For messages from other bridges, prefix with the bridge info and user name // Create cache key: "bridgeType:remoteID:userID"
if msg.SourceUserName != "" { cacheKey := fmt.Sprintf("%s:%s:%s", msg.SourceBridge, msg.SourceRemoteID, msg.SourceUserID)
bridgeIcon := h.getBridgeIcon(msg.SourceBridge)
return fmt.Sprintf("%s **%s**: %s", bridgeIcon, msg.SourceUserName, msg.Content) // Check cache first
h.cacheMu.RLock()
if userID, exists := h.userCache[cacheKey]; exists {
h.cacheMu.RUnlock()
return userID, nil
} }
return msg.Content 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)
// Generate email using bridge ID
email := fmt.Sprintf("%s@bridge.%s", username, h.bridge.bridgeID)
// First try to find existing user by username
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,
"source_bridge", msg.SourceBridge,
"source_remote_id", msg.SourceRemoteID)
return existingUser.Id, nil
}
}
// Also try to find user by email
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,
"source_bridge", msg.SourceBridge,
"source_remote_id", msg.SourceRemoteID)
return existingUser.Id, nil
}
}
// User doesn't exist, create the remote user
user := &mmModel.User{
Username: username,
Email: email,
FirstName: msg.SourceUserName,
Password: mmModel.NewId(),
RemoteId: &msg.SourceRemoteID,
}
// Try to create the user
createdUser, appErr := h.bridge.api.CreateUser(user)
if appErr != nil {
h.logger.LogError("Failed to create remote user",
"username", username,
"email", email,
"source_bridge", msg.SourceBridge,
"source_remote_id", msg.SourceRemoteID,
"error", appErr)
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,
"email", email,
"source_bridge", msg.SourceBridge,
"source_remote_id", msg.SourceRemoteID)
return createdUser.Id, nil
} }
// getBridgeIcon returns an icon/emoji for the source bridge // generateUsername creates a username from source information
func (h *mattermostMessageHandler) getBridgeIcon(bridgeType string) string { func (h *mattermostMessageHandler) generateUsername(sourceUserID, sourceUserName, sourceBridge string) string {
switch bridgeType { var baseUsername string
case "xmpp":
return ":speech_balloon:" // Chat bubble emoji for XMPP // Prefer source user name, fallback to user ID
case "slack": if sourceUserName != "" {
return ":slack:" // Slack emoji if available baseUsername = sourceUserName
case "discord": } else {
return ":discord:" // Discord emoji if available baseUsername = sourceUserID
default:
return ":bridge_at_night:" // Generic bridge emoji
} }
// Clean the username (remove invalid characters, make lowercase)
baseUsername = strings.ToLower(baseUsername)
baseUsername = strings.ReplaceAll(baseUsername, "@", "")
baseUsername = strings.ReplaceAll(baseUsername, ".", "")
baseUsername = strings.ReplaceAll(baseUsername, " ", "")
// Prefix with bridge type to avoid conflicts
return fmt.Sprintf("%s-%s", sourceBridge, baseUsername)
} }
// mattermostUserResolver handles user resolution for the Mattermost bridge // mattermostUserResolver handles user resolution for the Mattermost bridge