mattermost-plugin-bridge-xmpp/server/bridge/mattermost/bridge.go
Felipe Martin db8037ffbf
feat: implement comprehensive bridge-agnostic user management system
This commit implements a complete multi-user bridge management system that allows bridges to control multiple users with async goroutine management and convenience methods for channel operations.

Key features:
- Bridge-agnostic BridgeUser interface with validation, identity, state management, channel operations, connection lifecycle, and goroutine lifecycle methods
- BridgeUserManager interface for user lifecycle management with bridge type identification
- XMPPUser implementation for XMPP bridge with XMPP client integration, connection monitoring, and room operations
- MattermostUser implementation for Mattermost bridge with API integration and channel management
- Updated Bridge interface to include GetUserManager() method
- Base UserManager implementation with generic user management logic
- Added Ping() and CheckChannelExists() methods to BridgeUser interface for health checking and room validation
- Updated bridge manager naming from Manager to BridgeManager for clarity

The system enables bridges to manage multiple users (like "Mattermost Bridge" user in XMPP) with proper state management, connection monitoring, and channel operations abstracted across different bridge protocols.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-04 17:50:44 +02:00

338 lines
9.8 KiB
Go

package mattermost
import (
"context"
"fmt"
"sync"
"sync/atomic"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
pluginModel "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/store/kvstore"
"github.com/mattermost/mattermost/server/public/plugin"
)
// mattermostBridge handles syncing messages between Mattermost instances
type mattermostBridge struct {
logger logger.Logger
api plugin.API
kvstore kvstore.KVStore
userManager pluginModel.BridgeUserManager
// Connection management
connected atomic.Bool
ctx context.Context
cancel context.CancelFunc
// Current configuration
config *config.Configuration
configMu sync.RWMutex
// Channel mappings cache
channelMappings map[string]string
mappingsMu sync.RWMutex
}
// NewBridge creates a new Mattermost bridge
func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *config.Configuration) pluginModel.Bridge {
ctx, cancel := context.WithCancel(context.Background())
bridge := &mattermostBridge{
logger: log,
api: api,
kvstore: kvstore,
ctx: ctx,
cancel: cancel,
channelMappings: make(map[string]string),
config: cfg,
userManager: bridge.NewUserManager("mattermost", log),
}
return bridge
}
// UpdateConfiguration updates the bridge configuration
func (b *mattermostBridge) UpdateConfiguration(newConfig any) error {
cfg, ok := newConfig.(*config.Configuration)
if !ok {
return fmt.Errorf("invalid configuration type")
}
b.configMu.Lock()
b.config = cfg
b.configMu.Unlock()
// Log the configuration change
b.logger.LogInfo("Mattermost bridge configuration updated")
return nil
}
// Start initializes the bridge
func (b *mattermostBridge) Start() error {
b.logger.LogDebug("Starting Mattermost bridge")
b.configMu.RLock()
config := b.config
b.configMu.RUnlock()
if config == nil {
return fmt.Errorf("bridge configuration not set")
}
// For Mattermost bridge, we're always "connected" since we're running within Mattermost
b.connected.Store(true)
// Load existing channel mappings
if err := b.loadChannelMappings(); err != nil {
b.logger.LogWarn("Failed to load some channel mappings", "error", err)
}
b.logger.LogInfo("Mattermost bridge started successfully")
return nil
}
// Stop shuts down the bridge
func (b *mattermostBridge) Stop() error {
b.logger.LogInfo("Stopping Mattermost bridge")
if b.cancel != nil {
b.cancel()
}
b.connected.Store(false)
b.logger.LogInfo("Mattermost bridge stopped")
return nil
}
// loadChannelMappings loads existing channel mappings from KV store
func (b *mattermostBridge) loadChannelMappings() error {
b.logger.LogDebug("Loading channel mappings for Mattermost bridge")
// Get all channel mappings from KV store for Mattermost bridge
mappings, err := b.getAllChannelMappings()
if err != nil {
return fmt.Errorf("failed to load channel mappings: %w", err)
}
if len(mappings) == 0 {
b.logger.LogInfo("No channel mappings found for Mattermost bridge")
return nil
}
b.logger.LogInfo("Found channel mappings for Mattermost bridge", "count", len(mappings))
// Update local cache
b.mappingsMu.Lock()
for channelID, roomID := range mappings {
b.channelMappings[channelID] = roomID
}
b.mappingsMu.Unlock()
return nil
}
// getAllChannelMappings retrieves all channel mappings from KV store for Mattermost bridge
func (b *mattermostBridge) getAllChannelMappings() (map[string]string, error) {
if b.kvstore == nil {
return nil, fmt.Errorf("KV store not initialized")
}
mappings := make(map[string]string)
// Get all keys with the Mattermost bridge mapping prefix
mattermostPrefix := kvstore.KeyPrefixChannelMap + "mattermost_"
keys, err := b.kvstore.ListKeysWithPrefix(0, 1000, mattermostPrefix)
if err != nil {
return nil, fmt.Errorf("failed to list Mattermost bridge mapping keys: %w", err)
}
// Load each mapping
for _, key := range keys {
channelIDBytes, err := b.kvstore.Get(key)
if err != nil {
b.logger.LogWarn("Failed to load mapping for key", "key", key, "error", err)
continue
}
// Extract room ID from the key
roomID := kvstore.ExtractIdentifierFromChannelMapKey(key, "mattermost")
if roomID == "" {
b.logger.LogWarn("Failed to extract room ID from key", "key", key)
continue
}
channelID := string(channelIDBytes)
mappings[channelID] = roomID
}
return mappings, nil
}
// IsConnected returns whether the bridge is connected
func (b *mattermostBridge) IsConnected() bool {
// Mattermost bridge is always "connected" since it runs within Mattermost
return b.connected.Load()
}
// Ping actively tests the Mattermost API connectivity
func (b *mattermostBridge) Ping() error {
if !b.connected.Load() {
return fmt.Errorf("Mattermost bridge is not connected")
}
if b.api == nil {
return fmt.Errorf("Mattermost API not initialized")
}
b.logger.LogDebug("Testing Mattermost bridge connectivity with API ping")
// Test API connectivity with a lightweight call
// Using GetServerVersion as it's a simple, read-only operation
version := b.api.GetServerVersion()
if version == "" {
b.logger.LogWarn("Mattermost bridge ping returned empty version")
return fmt.Errorf("Mattermost API ping returned empty server version")
}
b.logger.LogDebug("Mattermost bridge ping successful", "server_version", version)
return nil
}
// CreateChannelMapping creates a mapping between a Mattermost channel and another Mattermost room/channel
func (b *mattermostBridge) CreateChannelMapping(channelID, roomID string) error {
if b.kvstore == nil {
return fmt.Errorf("KV store not initialized")
}
// Store forward and reverse mappings using bridge-agnostic keys
err := b.kvstore.Set(kvstore.BuildChannelMapKey("mattermost", channelID), []byte(roomID))
if err != nil {
return fmt.Errorf("failed to store channel room mapping: %w", err)
}
// Update local cache
b.mappingsMu.Lock()
b.channelMappings[channelID] = roomID
b.mappingsMu.Unlock()
b.logger.LogInfo("Created Mattermost channel room mapping", "channel_id", channelID, "room_id", roomID)
return nil
}
// GetChannelMapping gets the room ID for a Mattermost channel
func (b *mattermostBridge) GetChannelMapping(channelID string) (string, error) {
// Check cache first
b.mappingsMu.RLock()
roomID, exists := b.channelMappings[channelID]
b.mappingsMu.RUnlock()
if exists {
return roomID, nil
}
if b.kvstore == nil {
return "", fmt.Errorf("KV store not initialized")
}
// Check if we have a mapping in the KV store for this channel ID
roomIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("mattermost", channelID))
if err != nil {
return "", nil // Unmapped channels are expected
}
roomID = string(roomIDBytes)
// Update cache
b.mappingsMu.Lock()
b.channelMappings[channelID] = roomID
b.mappingsMu.Unlock()
return roomID, nil
}
// DeleteChannelMapping removes a mapping between a Mattermost channel and room
func (b *mattermostBridge) DeleteChannelMapping(channelID string) error {
if b.kvstore == nil {
return fmt.Errorf("KV store not initialized")
}
// Get the room ID from the mapping before deleting
roomID, err := b.GetChannelMapping(channelID)
if err != nil {
return fmt.Errorf("failed to get channel mapping: %w", err)
}
if roomID == "" {
return fmt.Errorf("channel is not mapped to any room")
}
// Delete forward and reverse mappings from KV store
err = b.kvstore.Delete(kvstore.BuildChannelMapKey("mattermost", channelID))
if err != nil {
return fmt.Errorf("failed to delete channel room mapping: %w", err)
}
// Remove from local cache
b.mappingsMu.Lock()
delete(b.channelMappings, channelID)
b.mappingsMu.Unlock()
b.logger.LogInfo("Deleted Mattermost channel room mapping", "channel_id", channelID, "room_id", roomID)
return nil
}
// RoomExists checks if a Mattermost channel exists on the server
func (b *mattermostBridge) RoomExists(roomID string) (bool, error) {
if b.api == nil {
return false, fmt.Errorf("Mattermost API not initialized")
}
b.logger.LogDebug("Checking if Mattermost channel exists", "channel_id", roomID)
// Use the Mattermost API to check if the channel exists
channel, appErr := b.api.GetChannel(roomID)
if appErr != nil {
if appErr.StatusCode == 404 {
b.logger.LogDebug("Mattermost channel does not exist", "channel_id", roomID)
return false, nil
}
b.logger.LogError("Failed to check channel existence", "channel_id", roomID, "error", appErr)
return false, fmt.Errorf("failed to check channel existence: %w", appErr)
}
if channel == nil {
b.logger.LogDebug("Mattermost channel does not exist (nil response)", "channel_id", roomID)
return false, nil
}
b.logger.LogDebug("Mattermost channel exists", "channel_id", roomID, "channel_name", channel.Name)
return true, nil
}
// GetRoomMapping retrieves the Mattermost channel ID for a given room ID (reverse lookup)
func (b *mattermostBridge) GetRoomMapping(roomID string) (string, error) {
if b.kvstore == nil {
return "", fmt.Errorf("KV store not initialized")
}
b.logger.LogDebug("Getting channel mapping for Mattermost room", "room_id", roomID)
// Look up the channel ID using the room ID as the key
channelIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("mattermost", roomID))
if err != nil {
// No mapping found is not an error, just return empty string
b.logger.LogDebug("No channel mapping found for room", "room_id", roomID)
return "", nil
}
channelID := string(channelIDBytes)
b.logger.LogDebug("Found channel mapping for room", "room_id", roomID, "channel_id", channelID)
return channelID, nil
}
// GetUserManager returns the user manager for this bridge
func (b *mattermostBridge) GetUserManager() pluginModel.BridgeUserManager {
return b.userManager
}