feat: implement centralized channel mapping management
Adds OnChannelMappingDeleted method to BridgeManager for centralized cleanup of channel mappings across all bridge types. Updates slash commands to use centralized management and fixes method naming inconsistencies. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5d143808a3
commit
2e13d96dce
7 changed files with 480 additions and 43 deletions
|
@ -213,4 +213,100 @@ func (m *Manager) OnPluginConfigurationChange(config any) error {
|
|||
|
||||
m.logger.LogInfo("Configuration changes propagated to all bridges")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// OnChannelMappingCreated handles the creation of a channel mapping by calling the appropriate bridge
|
||||
func (m *Manager) OnChannelMappingCreated(channelID, bridgeName, bridgeRoomID string) error {
|
||||
// Input validation
|
||||
if channelID == "" {
|
||||
return fmt.Errorf("channelID cannot be empty")
|
||||
}
|
||||
if bridgeName == "" {
|
||||
return fmt.Errorf("bridgeName cannot be empty")
|
||||
}
|
||||
if bridgeRoomID == "" {
|
||||
return fmt.Errorf("bridgeRoomID cannot be empty")
|
||||
}
|
||||
|
||||
m.logger.LogDebug("Creating channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID)
|
||||
|
||||
// Get the specific bridge
|
||||
bridge, err := m.GetBridge(bridgeName)
|
||||
if err != nil {
|
||||
m.logger.LogError("Failed to get bridge", "bridge_name", bridgeName, "error", err)
|
||||
return fmt.Errorf("failed to get bridge '%s': %w", bridgeName, err)
|
||||
}
|
||||
|
||||
// Check if bridge is connected
|
||||
if !bridge.IsConnected() {
|
||||
return fmt.Errorf("bridge '%s' is not connected", bridgeName)
|
||||
}
|
||||
|
||||
// Create the channel mapping on the receiving bridge
|
||||
if err = bridge.CreateChannelMapping(channelID, bridgeRoomID); err != nil {
|
||||
m.logger.LogError("Failed to create channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID, "error", err)
|
||||
return fmt.Errorf("failed to create channel mapping for bridge '%s': %w", bridgeName, err)
|
||||
}
|
||||
|
||||
mattermostBridge, err := m.GetBridge("mattermost")
|
||||
if err != nil {
|
||||
m.logger.LogError("Failed to get Mattermost bridge", "error", err)
|
||||
return fmt.Errorf("failed to get Mattermost bridge: %w", err)
|
||||
}
|
||||
|
||||
// Create the channel mapping in the Mattermost bridge
|
||||
if err = mattermostBridge.CreateChannelMapping(channelID, bridgeRoomID); err != nil {
|
||||
m.logger.LogError("Failed to create channel mapping in Mattermost bridge", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID, "error", err)
|
||||
return fmt.Errorf("failed to create channel mapping in Mattermost bridge: %w", err)
|
||||
}
|
||||
|
||||
m.logger.LogInfo("Successfully created channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "bridge_room_id", bridgeRoomID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnChannelMappingDeleted handles the deletion of a channel mapping by calling the appropriate bridges
|
||||
func (m *Manager) OnChannelMappingDeleted(channelID, bridgeName string) error {
|
||||
// Input validation
|
||||
if channelID == "" {
|
||||
return fmt.Errorf("channelID cannot be empty")
|
||||
}
|
||||
if bridgeName == "" {
|
||||
return fmt.Errorf("bridgeName cannot be empty")
|
||||
}
|
||||
|
||||
m.logger.LogDebug("Deleting channel mapping", "channel_id", channelID, "bridge_name", bridgeName)
|
||||
|
||||
// Get the specific bridge
|
||||
bridge, err := m.GetBridge(bridgeName)
|
||||
if err != nil {
|
||||
m.logger.LogError("Failed to get bridge", "bridge_name", bridgeName, "error", err)
|
||||
return fmt.Errorf("failed to get bridge '%s': %w", bridgeName, err)
|
||||
}
|
||||
|
||||
// Check if bridge is connected
|
||||
if !bridge.IsConnected() {
|
||||
return fmt.Errorf("bridge '%s' is not connected", bridgeName)
|
||||
}
|
||||
|
||||
// Delete the channel mapping from the specific bridge
|
||||
if err = bridge.DeleteChannelMapping(channelID); err != nil {
|
||||
m.logger.LogError("Failed to delete channel mapping", "channel_id", channelID, "bridge_name", bridgeName, "error", err)
|
||||
return fmt.Errorf("failed to delete channel mapping for bridge '%s': %w", bridgeName, err)
|
||||
}
|
||||
|
||||
// Also delete from Mattermost bridge to clean up reverse mappings
|
||||
mattermostBridge, err := m.GetBridge("mattermost")
|
||||
if err != nil {
|
||||
m.logger.LogError("Failed to get Mattermost bridge", "error", err)
|
||||
return fmt.Errorf("failed to get Mattermost bridge: %w", err)
|
||||
}
|
||||
|
||||
// Delete the channel mapping from the Mattermost bridge
|
||||
if err = mattermostBridge.DeleteChannelMapping(channelID); err != nil {
|
||||
m.logger.LogError("Failed to delete channel mapping from Mattermost bridge", "channel_id", channelID, "bridge_name", bridgeName, "error", err)
|
||||
return fmt.Errorf("failed to delete channel mapping from Mattermost bridge: %w", err)
|
||||
}
|
||||
|
||||
m.logger.LogInfo("Successfully deleted channel mapping", "channel_id", channelID, "bridge_name", bridgeName)
|
||||
return nil
|
||||
}
|
||||
|
|
257
server/bridge/mattermost/bridge.go
Normal file
257
server/bridge/mattermost/bridge.go
Normal file
|
@ -0,0 +1,257 @@
|
|||
package mattermost
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"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
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
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()
|
||||
oldConfig := b.config
|
||||
b.config = cfg
|
||||
b.configMu.Unlock()
|
||||
|
||||
// Log the configuration change
|
||||
b.logger.LogInfo("Mattermost bridge configuration updated", "old_config", oldConfig, "new_config", cfg)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -142,9 +142,6 @@ func (b *xmppBridge) Start() error {
|
|||
return fmt.Errorf("bridge configuration not set")
|
||||
}
|
||||
|
||||
// Print the configuration for debugging
|
||||
b.logger.LogDebug("Bridge configuration", "config", config)
|
||||
|
||||
if !config.EnableSync {
|
||||
b.logger.LogInfo("XMPP sync is disabled, bridge will not start")
|
||||
return nil
|
||||
|
@ -378,19 +375,13 @@ func (b *xmppBridge) IsConnected() bool {
|
|||
return b.connected.Load()
|
||||
}
|
||||
|
||||
// CreateChannelRoomMapping creates a mapping between a Mattermost channel and XMPP room
|
||||
func (b *xmppBridge) CreateChannelRoomMapping(channelID, roomJID string) error {
|
||||
// CreateChannelMapping creates a mapping between a Mattermost channel and XMPP room
|
||||
func (b *xmppBridge) CreateChannelMapping(channelID, roomJID 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(roomJID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store channel room mapping: %w", err)
|
||||
}
|
||||
|
||||
err = b.kvstore.Set(kvstore.BuildChannelMapKey("xmpp", roomJID), []byte(channelID))
|
||||
err := b.kvstore.Set(kvstore.BuildChannelMapKey("xmpp", roomJID), []byte(channelID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store reverse room mapping: %w", err)
|
||||
}
|
||||
|
@ -411,8 +402,8 @@ func (b *xmppBridge) CreateChannelRoomMapping(channelID, roomJID string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetChannelRoomMapping gets the XMPP room JID for a Mattermost channel
|
||||
func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) {
|
||||
// GetChannelMapping gets the XMPP room JID for a Mattermost channel
|
||||
func (b *xmppBridge) GetChannelMapping(channelID string) (string, error) {
|
||||
// Check cache first
|
||||
b.mappingsMu.RLock()
|
||||
roomJID, exists := b.channelMappings[channelID]
|
||||
|
@ -427,7 +418,7 @@ func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) {
|
|||
}
|
||||
|
||||
// Check if we have a mapping in the KV store for this channel ID
|
||||
roomJIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("mattermost", channelID))
|
||||
roomJIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("xmpp", channelID))
|
||||
if err != nil {
|
||||
return "", nil // Unmapped channels are expected
|
||||
}
|
||||
|
@ -442,14 +433,14 @@ func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) {
|
|||
return roomJID, nil
|
||||
}
|
||||
|
||||
// DeleteChannelRoomMapping removes a mapping between a Mattermost channel and XMPP room
|
||||
func (b *xmppBridge) DeleteChannelRoomMapping(channelID string) error {
|
||||
// DeleteChannelMapping removes a mapping between a Mattermost channel and XMPP room
|
||||
func (b *xmppBridge) DeleteChannelMapping(channelID string) error {
|
||||
if b.kvstore == nil {
|
||||
return fmt.Errorf("KV store not initialized")
|
||||
}
|
||||
|
||||
// Get the room JID from the mapping before deleting
|
||||
roomJID, err := b.GetChannelRoomMapping(channelID)
|
||||
roomJID, err := b.GetChannelMapping(channelID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get channel mapping: %w", err)
|
||||
}
|
||||
|
@ -457,12 +448,6 @@ func (b *xmppBridge) DeleteChannelRoomMapping(channelID string) error {
|
|||
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)
|
||||
}
|
||||
|
||||
err = b.kvstore.Delete(kvstore.BuildChannelMapKey("xmpp", roomJID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete reverse room mapping: %w", err)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue