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>
338 lines
9.8 KiB
Go
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
|
|
}
|