feat: implement production-ready MUC operations and comprehensive testing
- Implement proper XMPP MUC operations using mellium.im/xmpp/muc package - Add session readiness checking to prevent blocking on room joins - Create comprehensive bridge manager architecture with lifecycle management - Add complete channel mapping functionality with KV store persistence - Remove defensive logger nil checks as requested by user - Enhance XMPP client doctor with MUC testing (join/wait/leave workflow) - Add detailed dev server documentation for test room creation - Implement timeout protection for all MUC operations - Add proper error handling with fmt.Errorf instead of pkg/errors - Successfully tested: MUC join in ~21ms, 5s wait, clean leave operation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4d6929bab6
commit
d159c668c2
11 changed files with 1048 additions and 553 deletions
|
@ -17,6 +17,7 @@ const (
|
||||||
defaultUsername = "testuser@localhost"
|
defaultUsername = "testuser@localhost"
|
||||||
defaultPassword = "testpass"
|
defaultPassword = "testpass"
|
||||||
defaultResource = "doctor"
|
defaultResource = "doctor"
|
||||||
|
defaultTestRoom = "test1@conference.localhost"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -24,6 +25,8 @@ type Config struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
Resource string
|
Resource string
|
||||||
|
TestRoom string
|
||||||
|
TestMUC bool
|
||||||
Verbose bool
|
Verbose bool
|
||||||
InsecureSkipVerify bool
|
InsecureSkipVerify bool
|
||||||
}
|
}
|
||||||
|
@ -36,19 +39,27 @@ func main() {
|
||||||
flag.StringVar(&config.Username, "username", defaultUsername, "XMPP username/JID")
|
flag.StringVar(&config.Username, "username", defaultUsername, "XMPP username/JID")
|
||||||
flag.StringVar(&config.Password, "password", defaultPassword, "XMPP password")
|
flag.StringVar(&config.Password, "password", defaultPassword, "XMPP password")
|
||||||
flag.StringVar(&config.Resource, "resource", defaultResource, "XMPP resource")
|
flag.StringVar(&config.Resource, "resource", defaultResource, "XMPP resource")
|
||||||
|
flag.StringVar(&config.TestRoom, "test-room", defaultTestRoom, "MUC room JID for testing")
|
||||||
|
flag.BoolVar(&config.TestMUC, "test-muc", true, "Enable MUC room testing (join/wait/leave)")
|
||||||
flag.BoolVar(&config.Verbose, "verbose", true, "Enable verbose logging")
|
flag.BoolVar(&config.Verbose, "verbose", true, "Enable verbose logging")
|
||||||
flag.BoolVar(&config.InsecureSkipVerify, "insecure-skip-verify", true, "Skip TLS certificate verification (for development)")
|
flag.BoolVar(&config.InsecureSkipVerify, "insecure-skip-verify", true, "Skip TLS certificate verification (for development)")
|
||||||
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "xmpp-client-doctor - Test XMPP client connectivity\n\n")
|
fmt.Fprintf(os.Stderr, "xmpp-client-doctor - Test XMPP client connectivity and MUC operations\n\n")
|
||||||
fmt.Fprintf(os.Stderr, "This tool tests the XMPP client implementation by connecting to an XMPP server,\n")
|
fmt.Fprintf(os.Stderr, "This tool tests the XMPP client implementation by connecting to an XMPP server,\n")
|
||||||
fmt.Fprintf(os.Stderr, "performing a connection test, and then disconnecting gracefully.\n\n")
|
fmt.Fprintf(os.Stderr, "performing connection tests, optionally testing MUC room operations,\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "and then disconnecting gracefully.\n\n")
|
||||||
fmt.Fprintf(os.Stderr, "Usage:\n")
|
fmt.Fprintf(os.Stderr, "Usage:\n")
|
||||||
fmt.Fprintf(os.Stderr, " %s [flags]\n\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s [flags]\n\n", os.Args[0])
|
||||||
|
fmt.Fprintf(os.Stderr, "Examples:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " %s # Test basic connectivity\n", os.Args[0])
|
||||||
|
fmt.Fprintf(os.Stderr, " %s --test-muc # Test connectivity and MUC operations\n", os.Args[0])
|
||||||
|
fmt.Fprintf(os.Stderr, " %s --test-muc=false # Test connectivity only\n\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, "Flags:\n")
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
fmt.Fprintf(os.Stderr, "\nDefault values are configured for the development server in ./sidecar/\n")
|
fmt.Fprintf(os.Stderr, "\nDefault values are configured for the development server in ./sidecar/\n")
|
||||||
fmt.Fprintf(os.Stderr, "Make sure to start the development server with: cd sidecar && docker-compose up -d\n")
|
fmt.Fprintf(os.Stderr, "Make sure to start the development server with: cd sidecar && docker-compose up -d\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "For MUC testing, create the test room 'test1' via the admin console at http://localhost:9090\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
@ -61,6 +72,9 @@ func main() {
|
||||||
log.Printf(" Username: %s", config.Username)
|
log.Printf(" Username: %s", config.Username)
|
||||||
log.Printf(" Resource: %s", config.Resource)
|
log.Printf(" Resource: %s", config.Resource)
|
||||||
log.Printf(" Password: %s", maskPassword(config.Password))
|
log.Printf(" Password: %s", maskPassword(config.Password))
|
||||||
|
if config.TestMUC {
|
||||||
|
log.Printf(" Test Room: %s", config.TestRoom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the XMPP client
|
// Test the XMPP client
|
||||||
|
@ -72,6 +86,9 @@ func main() {
|
||||||
log.Printf("✅ XMPP client test completed successfully!")
|
log.Printf("✅ XMPP client test completed successfully!")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("✅ XMPP client connectivity test passed!")
|
fmt.Println("✅ XMPP client connectivity test passed!")
|
||||||
|
if config.TestMUC {
|
||||||
|
fmt.Println("✅ XMPP MUC operations test passed!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +151,21 @@ func testXMPPClient(config *Config) error {
|
||||||
|
|
||||||
if config.Verbose {
|
if config.Verbose {
|
||||||
log.Printf("✅ Connection health test passed in %v", pingDuration)
|
log.Printf("✅ Connection health test passed in %v", pingDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mucDuration time.Duration
|
||||||
|
|
||||||
|
// Test MUC operations if requested
|
||||||
|
if config.TestMUC {
|
||||||
|
start = time.Now()
|
||||||
|
err = testMUCOperations(client, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("MUC operations test failed: %w", err)
|
||||||
|
}
|
||||||
|
mucDuration = time.Since(start)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Verbose {
|
||||||
log.Printf("Disconnecting from XMPP server...")
|
log.Printf("Disconnecting from XMPP server...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,8 +182,61 @@ func testXMPPClient(config *Config) error {
|
||||||
log.Printf("Connection summary:")
|
log.Printf("Connection summary:")
|
||||||
log.Printf(" Connect time: %v", connectDuration)
|
log.Printf(" Connect time: %v", connectDuration)
|
||||||
log.Printf(" Ping time: %v", pingDuration)
|
log.Printf(" Ping time: %v", pingDuration)
|
||||||
|
if config.TestMUC {
|
||||||
|
log.Printf(" MUC operations time: %v", mucDuration)
|
||||||
|
}
|
||||||
log.Printf(" Disconnect time: %v", disconnectDuration)
|
log.Printf(" Disconnect time: %v", disconnectDuration)
|
||||||
log.Printf(" Total time: %v", connectDuration+pingDuration+disconnectDuration)
|
totalTime := connectDuration + pingDuration + disconnectDuration
|
||||||
|
if config.TestMUC {
|
||||||
|
totalTime += mucDuration
|
||||||
|
}
|
||||||
|
log.Printf(" Total time: %v", totalTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMUCOperations(client *xmpp.Client, config *Config) error {
|
||||||
|
if config.Verbose {
|
||||||
|
log.Printf("Testing MUC operations with room: %s", config.TestRoom)
|
||||||
|
log.Printf("Attempting to join MUC room...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test joining the room
|
||||||
|
start := time.Now()
|
||||||
|
err := client.JoinRoom(config.TestRoom)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to join MUC room %s: %w", config.TestRoom, err)
|
||||||
|
}
|
||||||
|
joinDuration := time.Since(start)
|
||||||
|
|
||||||
|
if config.Verbose {
|
||||||
|
log.Printf("✅ Successfully joined MUC room in %v", joinDuration)
|
||||||
|
log.Printf("Waiting 5 seconds in the room...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 5 seconds
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
if config.Verbose {
|
||||||
|
log.Printf("Attempting to leave MUC room...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test leaving the room
|
||||||
|
start = time.Now()
|
||||||
|
err = client.LeaveRoom(config.TestRoom)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to leave MUC room %s: %w", config.TestRoom, err)
|
||||||
|
}
|
||||||
|
leaveDuration := time.Since(start)
|
||||||
|
|
||||||
|
if config.Verbose {
|
||||||
|
log.Printf("✅ Successfully left MUC room in %v", leaveDuration)
|
||||||
|
log.Printf("MUC operations summary:")
|
||||||
|
log.Printf(" Join time: %v", joinDuration)
|
||||||
|
log.Printf(" Wait time: 5s")
|
||||||
|
log.Printf(" Leave time: %v", leaveDuration)
|
||||||
|
log.Printf(" Total MUC time: %v", joinDuration+5*time.Second+leaveDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
216
server/bridge/manager.go
Normal file
216
server/bridge/manager.go
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
||||||
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager manages multiple bridge instances
|
||||||
|
type Manager struct {
|
||||||
|
bridges map[string]model.Bridge
|
||||||
|
mu sync.RWMutex
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new bridge manager
|
||||||
|
func NewManager(logger logger.Logger) model.BridgeManager {
|
||||||
|
if logger == nil {
|
||||||
|
panic("logger cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Manager{
|
||||||
|
bridges: make(map[string]model.Bridge),
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterBridge registers a bridge with the manager
|
||||||
|
func (m *Manager) RegisterBridge(name string, bridge model.Bridge) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("bridge name cannot be empty")
|
||||||
|
}
|
||||||
|
if bridge == nil {
|
||||||
|
return fmt.Errorf("bridge cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if _, exists := m.bridges[name]; exists {
|
||||||
|
return fmt.Errorf("bridge '%s' is already registered", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.bridges[name] = bridge
|
||||||
|
m.logger.LogInfo("Bridge registered", "name", name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartBridge starts a specific bridge
|
||||||
|
func (m *Manager) StartBridge(name string) error {
|
||||||
|
m.mu.RLock()
|
||||||
|
bridge, exists := m.bridges[name]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("bridge '%s' is not registered", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.LogInfo("Starting bridge", "name", name)
|
||||||
|
|
||||||
|
if err := bridge.Start(); err != nil {
|
||||||
|
m.logger.LogError("Failed to start bridge", "name", name, "error", err)
|
||||||
|
return fmt.Errorf("failed to start bridge '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.LogInfo("Bridge started successfully", "name", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopBridge stops a specific bridge
|
||||||
|
func (m *Manager) StopBridge(name string) error {
|
||||||
|
m.mu.RLock()
|
||||||
|
bridge, exists := m.bridges[name]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("bridge '%s' is not registered", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.LogInfo("Stopping bridge", "name", name)
|
||||||
|
|
||||||
|
if err := bridge.Stop(); err != nil {
|
||||||
|
m.logger.LogError("Failed to stop bridge", "name", name, "error", err)
|
||||||
|
return fmt.Errorf("failed to stop bridge '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.LogInfo("Bridge stopped successfully", "name", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnregisterBridge removes a bridge from the manager
|
||||||
|
func (m *Manager) UnregisterBridge(name string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
bridge, exists := m.bridges[name]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("bridge '%s' is not registered", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the bridge before unregistering
|
||||||
|
if bridge.IsConnected() {
|
||||||
|
if err := bridge.Stop(); err != nil {
|
||||||
|
m.logger.LogWarn("Failed to stop bridge during unregistration", "name", name, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(m.bridges, name)
|
||||||
|
m.logger.LogInfo("Bridge unregistered", "name", name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBridge retrieves a bridge by name
|
||||||
|
func (m *Manager) GetBridge(name string) (model.Bridge, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
bridge, exists := m.bridges[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("bridge '%s' is not registered", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bridge, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBridges returns a list of all registered bridge names
|
||||||
|
func (m *Manager) ListBridges() []string {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
bridges := make([]string, 0, len(m.bridges))
|
||||||
|
for name := range m.bridges {
|
||||||
|
bridges = append(bridges, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bridges
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasBridge checks if a bridge with the given name is registered
|
||||||
|
func (m *Manager) HasBridge(name string) bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
_, exists := m.bridges[name]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasBridges checks if any bridges are registered
|
||||||
|
func (m *Manager) HasBridges() bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
return len(m.bridges) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown stops and unregisters all bridges
|
||||||
|
func (m *Manager) Shutdown() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.logger.LogInfo("Shutting down bridge manager", "bridge_count", len(m.bridges))
|
||||||
|
|
||||||
|
var errors []error
|
||||||
|
for name, bridge := range m.bridges {
|
||||||
|
if bridge.IsConnected() {
|
||||||
|
if err := bridge.Stop(); err != nil {
|
||||||
|
errors = append(errors, fmt.Errorf("failed to stop bridge '%s': %w", name, err))
|
||||||
|
m.logger.LogError("Failed to stop bridge during shutdown", "name", name, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all bridges
|
||||||
|
m.bridges = make(map[string]model.Bridge)
|
||||||
|
|
||||||
|
m.logger.LogInfo("Bridge manager shutdown complete")
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return fmt.Errorf("shutdown completed with errors: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPluginConfigurationChange propagates configuration changes to all registered bridges
|
||||||
|
func (m *Manager) OnPluginConfigurationChange(config any) error {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if len(m.bridges) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.LogInfo("Plugin configuration changed, propagating to bridges", "bridge_count", len(m.bridges))
|
||||||
|
|
||||||
|
var errors []error
|
||||||
|
for name, bridge := range m.bridges {
|
||||||
|
if err := bridge.UpdateConfiguration(config); err != nil {
|
||||||
|
errors = append(errors, fmt.Errorf("failed to update configuration for bridge '%s': %w", name, err))
|
||||||
|
m.logger.LogError("Failed to update bridge configuration", "name", name, "error", err)
|
||||||
|
} else {
|
||||||
|
m.logger.LogDebug("Successfully updated bridge configuration", "name", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return fmt.Errorf("configuration update completed with errors: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.LogInfo("Configuration changes propagated to all bridges")
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,450 +0,0 @@
|
||||||
package mattermost
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config"
|
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/store/kvstore"
|
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp"
|
|
||||||
"github.com/mattermost/mattermost/server/public/plugin"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MattermostToXMPPBridge handles syncing messages from Mattermost to XMPP
|
|
||||||
type MattermostToXMPPBridge struct {
|
|
||||||
logger logger.Logger
|
|
||||||
api plugin.API
|
|
||||||
kvstore kvstore.KVStore
|
|
||||||
xmppClient *xmpp.Client
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMattermostToXMPPBridge creates a new Mattermost to XMPP bridge
|
|
||||||
func NewMattermostToXMPPBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *config.Configuration) *MattermostToXMPPBridge {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
bridge := &MattermostToXMPPBridge{
|
|
||||||
logger: log,
|
|
||||||
api: api,
|
|
||||||
kvstore: kvstore,
|
|
||||||
ctx: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
channelMappings: make(map[string]string),
|
|
||||||
config: cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize XMPP client with configuration
|
|
||||||
if cfg.EnableSync && cfg.XMPPServerURL != "" && cfg.XMPPUsername != "" && cfg.XMPPPassword != "" {
|
|
||||||
bridge.xmppClient = bridge.createXMPPClient(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return bridge
|
|
||||||
}
|
|
||||||
|
|
||||||
// createXMPPClient creates an XMPP client with the given configuration
|
|
||||||
func (b *MattermostToXMPPBridge) createXMPPClient(cfg *config.Configuration) *xmpp.Client {
|
|
||||||
// Create TLS config based on certificate verification setting
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
InsecureSkipVerify: cfg.XMPPInsecureSkipVerify,
|
|
||||||
}
|
|
||||||
|
|
||||||
return xmpp.NewClientWithTLS(
|
|
||||||
cfg.XMPPServerURL,
|
|
||||||
cfg.XMPPUsername,
|
|
||||||
cfg.XMPPPassword,
|
|
||||||
cfg.GetXMPPResource(),
|
|
||||||
"", // remoteID not needed for bridge user
|
|
||||||
tlsConfig,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateConfiguration updates the bridge configuration
|
|
||||||
func (b *MattermostToXMPPBridge) UpdateConfiguration(newConfig any) error {
|
|
||||||
cfg, ok := newConfig.(*config.Configuration)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("invalid configuration type")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.configMu.Lock()
|
|
||||||
oldConfig := b.config
|
|
||||||
b.config = cfg
|
|
||||||
|
|
||||||
// Initialize or update XMPP client with new configuration
|
|
||||||
if cfg.EnableSync {
|
|
||||||
if cfg.XMPPServerURL == "" || cfg.XMPPUsername == "" || cfg.XMPPPassword == "" {
|
|
||||||
b.configMu.Unlock()
|
|
||||||
return errors.New("XMPP server URL, username, and password are required when sync is enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.xmppClient = b.createXMPPClient(cfg)
|
|
||||||
} else {
|
|
||||||
b.xmppClient = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b.configMu.Unlock()
|
|
||||||
|
|
||||||
// Check if we need to restart the bridge due to configuration changes
|
|
||||||
wasConnected := b.connected.Load()
|
|
||||||
needsRestart := oldConfig != nil && !oldConfig.Equals(cfg) && wasConnected
|
|
||||||
|
|
||||||
// Log the configuration change
|
|
||||||
if b.logger != nil {
|
|
||||||
if needsRestart {
|
|
||||||
b.logger.LogInfo("Configuration changed, restarting bridge", "old_config", oldConfig, "new_config", cfg)
|
|
||||||
} else {
|
|
||||||
b.logger.LogInfo("Configuration updated", "config", cfg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if needsRestart {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Configuration changed, restarting bridge")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the bridge
|
|
||||||
if err := b.Stop(); err != nil && b.logger != nil {
|
|
||||||
b.logger.LogWarn("Error stopping bridge during restart", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the bridge with new configuration
|
|
||||||
if err := b.Start(); err != nil {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogError("Failed to restart bridge with new configuration", "error", err)
|
|
||||||
}
|
|
||||||
return errors.Wrap(err, "failed to restart bridge")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start initializes the bridge and connects to XMPP
|
|
||||||
func (b *MattermostToXMPPBridge) Start() error {
|
|
||||||
b.logger.LogDebug("Starting Mattermost to XMPP bridge")
|
|
||||||
|
|
||||||
b.configMu.RLock()
|
|
||||||
config := b.config
|
|
||||||
b.configMu.RUnlock()
|
|
||||||
|
|
||||||
if config == nil {
|
|
||||||
return errors.New("bridge configuration not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the configuration for debugging
|
|
||||||
b.logger.LogDebug("Bridge configuration", "config", config)
|
|
||||||
|
|
||||||
if !config.EnableSync {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("XMPP sync is disabled, bridge will not start")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Starting Mattermost to XMPP bridge", "xmpp_server", config.XMPPServerURL, "username", config.XMPPUsername)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to XMPP server
|
|
||||||
if err := b.connectToXMPP(); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to connect to XMPP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and join mapped channels
|
|
||||||
if err := b.loadAndJoinMappedChannels(); err != nil {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogWarn("Failed to join some mapped channels", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start connection monitor
|
|
||||||
go b.connectionMonitor()
|
|
||||||
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Mattermost to XMPP bridge started successfully")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop shuts down the bridge
|
|
||||||
func (b *MattermostToXMPPBridge) Stop() error {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Stopping Mattermost to XMPP bridge")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.cancel != nil {
|
|
||||||
b.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.xmppClient != nil {
|
|
||||||
if err := b.xmppClient.Disconnect(); err != nil && b.logger != nil {
|
|
||||||
b.logger.LogWarn("Error disconnecting from XMPP server", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.connected.Store(false)
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Mattermost to XMPP bridge stopped")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// connectToXMPP establishes connection to the XMPP server
|
|
||||||
func (b *MattermostToXMPPBridge) connectToXMPP() error {
|
|
||||||
if b.xmppClient == nil {
|
|
||||||
return errors.New("XMPP client is not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogDebug("Connecting to XMPP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := b.xmppClient.Connect()
|
|
||||||
if err != nil {
|
|
||||||
b.connected.Store(false)
|
|
||||||
return errors.Wrap(err, "failed to connect to XMPP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.connected.Store(true)
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Successfully connected to XMPP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set online presence after successful connection
|
|
||||||
if err := b.xmppClient.SetOnlinePresence(); err != nil {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogWarn("Failed to set online presence", "error", err)
|
|
||||||
}
|
|
||||||
// Don't fail the connection for presence issues
|
|
||||||
} else if b.logger != nil {
|
|
||||||
b.logger.LogDebug("Set bridge user online presence")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadAndJoinMappedChannels loads channel mappings and joins corresponding XMPP rooms
|
|
||||||
func (b *MattermostToXMPPBridge) loadAndJoinMappedChannels() error {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogDebug("Loading and joining mapped channels")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all channel mappings from KV store
|
|
||||||
mappings, err := b.getAllChannelMappings()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to load channel mappings")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mappings) == 0 {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("No channel mappings found, no rooms to join")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Found channel mappings, joining XMPP rooms", "count", len(mappings))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join each mapped room
|
|
||||||
for channelID, roomJID := range mappings {
|
|
||||||
if err := b.joinXMPPRoom(channelID, roomJID); err != nil && b.logger != nil {
|
|
||||||
b.logger.LogWarn("Failed to join room", "channel_id", channelID, "room_jid", roomJID, "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// joinXMPPRoom joins an XMPP room and updates the local cache
|
|
||||||
func (b *MattermostToXMPPBridge) joinXMPPRoom(channelID, roomJID string) error {
|
|
||||||
if !b.connected.Load() {
|
|
||||||
return errors.New("not connected to XMPP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := b.xmppClient.JoinRoom(roomJID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to join XMPP room")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local cache
|
|
||||||
b.mappingsMu.Lock()
|
|
||||||
b.channelMappings[channelID] = roomJID
|
|
||||||
b.mappingsMu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAllChannelMappings retrieves all channel mappings from KV store
|
|
||||||
func (b *MattermostToXMPPBridge) getAllChannelMappings() (map[string]string, error) {
|
|
||||||
mappings := make(map[string]string)
|
|
||||||
return mappings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// connectionMonitor monitors the XMPP connection
|
|
||||||
func (b *MattermostToXMPPBridge) connectionMonitor() {
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-b.ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
if err := b.checkConnection(); err != nil {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogWarn("XMPP connection check failed", "error", err)
|
|
||||||
}
|
|
||||||
b.handleReconnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkConnection verifies the XMPP connection is still active
|
|
||||||
func (b *MattermostToXMPPBridge) checkConnection() error {
|
|
||||||
if !b.connected.Load() {
|
|
||||||
return errors.New("not connected")
|
|
||||||
}
|
|
||||||
return b.xmppClient.TestConnection()
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleReconnection attempts to reconnect to XMPP and rejoin rooms
|
|
||||||
func (b *MattermostToXMPPBridge) handleReconnection() {
|
|
||||||
b.configMu.RLock()
|
|
||||||
config := b.config
|
|
||||||
b.configMu.RUnlock()
|
|
||||||
|
|
||||||
if config == nil || !config.EnableSync {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Attempting to reconnect to XMPP server")
|
|
||||||
}
|
|
||||||
b.connected.Store(false)
|
|
||||||
|
|
||||||
if b.xmppClient != nil {
|
|
||||||
b.xmppClient.Disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry connection with exponential backoff
|
|
||||||
maxRetries := 3
|
|
||||||
for i := 0; i < maxRetries; i++ {
|
|
||||||
backoff := time.Duration(1<<uint(i)) * time.Second
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-b.ctx.Done():
|
|
||||||
return
|
|
||||||
case <-time.After(backoff):
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.connectToXMPP(); err != nil {
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogWarn("Reconnection attempt failed", "attempt", i+1, "error", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.loadAndJoinMappedChannels(); err != nil && b.logger != nil {
|
|
||||||
b.logger.LogWarn("Failed to rejoin rooms after reconnection", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Successfully reconnected to XMPP server")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogError("Failed to reconnect to XMPP server after all attempts")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public API methods
|
|
||||||
|
|
||||||
// IsConnected returns whether the bridge is connected to XMPP
|
|
||||||
func (b *MattermostToXMPPBridge) IsConnected() bool {
|
|
||||||
return b.connected.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateChannelRoomMapping creates a mapping between a Mattermost channel and XMPP room
|
|
||||||
func (b *MattermostToXMPPBridge) CreateChannelRoomMapping(channelID, roomJID string) error {
|
|
||||||
if b.kvstore == nil {
|
|
||||||
return errors.New("KV store not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store forward and reverse mappings
|
|
||||||
err := b.kvstore.Set(kvstore.BuildChannelMappingKey(channelID), []byte(roomJID))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to store channel room mapping")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = b.kvstore.Set(kvstore.BuildRoomMappingKey(roomJID), []byte(channelID))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to store reverse room mapping")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local cache
|
|
||||||
b.mappingsMu.Lock()
|
|
||||||
b.channelMappings[channelID] = roomJID
|
|
||||||
b.mappingsMu.Unlock()
|
|
||||||
|
|
||||||
// Join the room if connected
|
|
||||||
if b.connected.Load() {
|
|
||||||
if err := b.xmppClient.JoinRoom(roomJID); err != nil && b.logger != nil {
|
|
||||||
b.logger.LogWarn("Failed to join newly mapped room", "channel_id", channelID, "room_jid", roomJID, "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.logger != nil {
|
|
||||||
b.logger.LogInfo("Created channel room mapping", "channel_id", channelID, "room_jid", roomJID)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChannelRoomMapping gets the XMPP room JID for a Mattermost channel
|
|
||||||
func (b *MattermostToXMPPBridge) GetChannelRoomMapping(channelID string) (string, error) {
|
|
||||||
// Check cache first
|
|
||||||
b.mappingsMu.RLock()
|
|
||||||
roomJID, exists := b.channelMappings[channelID]
|
|
||||||
b.mappingsMu.RUnlock()
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
return roomJID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.kvstore == nil {
|
|
||||||
return "", errors.New("KV store not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load from KV store
|
|
||||||
roomJIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMappingKey(channelID))
|
|
||||||
if err != nil {
|
|
||||||
return "", nil // Unmapped channels are expected
|
|
||||||
}
|
|
||||||
|
|
||||||
roomJID = string(roomJIDBytes)
|
|
||||||
|
|
||||||
// Update cache
|
|
||||||
b.mappingsMu.Lock()
|
|
||||||
b.channelMappings[channelID] = roomJID
|
|
||||||
b.mappingsMu.Unlock()
|
|
||||||
|
|
||||||
return roomJID, nil
|
|
||||||
}
|
|
|
@ -1,11 +1,441 @@
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
// XMPPToMattermostBridge handles syncing messages from XMPP to Mattermost
|
import (
|
||||||
type XMPPToMattermostBridge struct {
|
"context"
|
||||||
// TODO: Implement in Phase 4
|
"crypto/tls"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
xmppClient "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp"
|
||||||
|
"github.com/mattermost/mattermost/server/public/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// xmppBridge handles syncing messages between Mattermost and XMPP
|
||||||
|
type xmppBridge struct {
|
||||||
|
logger logger.Logger
|
||||||
|
api plugin.API
|
||||||
|
kvstore kvstore.KVStore
|
||||||
|
xmppClient *xmppClient.Client
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewXMPPToMattermostBridge creates a new XMPP to Mattermost bridge
|
// NewBridge creates a new XMPP bridge
|
||||||
func NewXMPPToMattermostBridge() *XMPPToMattermostBridge {
|
func NewBridge(log logger.Logger, api plugin.API, kvstore kvstore.KVStore, cfg *config.Configuration) pluginModel.Bridge {
|
||||||
return &XMPPToMattermostBridge{}
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
bridge := &xmppBridge{
|
||||||
|
logger: log,
|
||||||
|
api: api,
|
||||||
|
kvstore: kvstore,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
channelMappings: make(map[string]string),
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize XMPP client with configuration
|
||||||
|
if cfg.EnableSync && cfg.XMPPServerURL != "" && cfg.XMPPUsername != "" && cfg.XMPPPassword != "" {
|
||||||
|
bridge.xmppClient = bridge.createXMPPClient(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
// createXMPPClient creates an XMPP client with the given configuration
|
||||||
|
func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) *xmppClient.Client {
|
||||||
|
// Create TLS config based on certificate verification setting
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: cfg.XMPPInsecureSkipVerify,
|
||||||
|
}
|
||||||
|
|
||||||
|
return xmppClient.NewClientWithTLS(
|
||||||
|
cfg.XMPPServerURL,
|
||||||
|
cfg.XMPPUsername,
|
||||||
|
cfg.XMPPPassword,
|
||||||
|
cfg.GetXMPPResource(),
|
||||||
|
"", // remoteID not needed for bridge user
|
||||||
|
tlsConfig,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConfiguration updates the bridge configuration
|
||||||
|
func (b *xmppBridge) 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
|
||||||
|
|
||||||
|
// Initialize or update XMPP client with new configuration
|
||||||
|
if cfg.EnableSync {
|
||||||
|
if cfg.XMPPServerURL == "" || cfg.XMPPUsername == "" || cfg.XMPPPassword == "" {
|
||||||
|
b.configMu.Unlock()
|
||||||
|
return fmt.Errorf("XMPP server URL, username, and password are required when sync is enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.xmppClient = b.createXMPPClient(cfg)
|
||||||
|
} else {
|
||||||
|
b.xmppClient = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.configMu.Unlock()
|
||||||
|
|
||||||
|
// Check if we need to restart the bridge due to configuration changes
|
||||||
|
wasConnected := b.connected.Load()
|
||||||
|
needsRestart := oldConfig != nil && !oldConfig.Equals(cfg) && wasConnected
|
||||||
|
|
||||||
|
// Log the configuration change
|
||||||
|
if needsRestart {
|
||||||
|
b.logger.LogInfo("Configuration changed, restarting bridge", "old_config", oldConfig, "new_config", cfg)
|
||||||
|
} else {
|
||||||
|
b.logger.LogInfo("Configuration updated", "config", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsRestart {
|
||||||
|
b.logger.LogInfo("Configuration changed, restarting bridge")
|
||||||
|
|
||||||
|
// Stop the bridge
|
||||||
|
if err := b.Stop(); err != nil {
|
||||||
|
b.logger.LogWarn("Error stopping bridge during restart", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the bridge with new configuration
|
||||||
|
if err := b.Start(); err != nil {
|
||||||
|
b.logger.LogError("Failed to restart bridge with new configuration", "error", err)
|
||||||
|
return fmt.Errorf("failed to restart bridge: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes the bridge and connects to XMPP
|
||||||
|
func (b *xmppBridge) Start() error {
|
||||||
|
b.logger.LogDebug("Starting Mattermost to XMPP bridge")
|
||||||
|
|
||||||
|
b.configMu.RLock()
|
||||||
|
config := b.config
|
||||||
|
b.configMu.RUnlock()
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.LogInfo("Starting Mattermost to XMPP bridge", "xmpp_server", config.XMPPServerURL, "username", config.XMPPUsername)
|
||||||
|
|
||||||
|
// Connect to XMPP server
|
||||||
|
if err := b.connectToXMPP(); err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to XMPP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and join mapped channels
|
||||||
|
if err := b.loadAndJoinMappedChannels(); err != nil {
|
||||||
|
b.logger.LogWarn("Failed to join some mapped channels", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start connection monitor
|
||||||
|
go b.connectionMonitor()
|
||||||
|
|
||||||
|
b.logger.LogInfo("Mattermost to XMPP bridge started successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop shuts down the bridge
|
||||||
|
func (b *xmppBridge) Stop() error {
|
||||||
|
b.logger.LogInfo("Stopping Mattermost to XMPP bridge")
|
||||||
|
|
||||||
|
if b.cancel != nil {
|
||||||
|
b.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.xmppClient != nil {
|
||||||
|
if err := b.xmppClient.Disconnect(); err != nil {
|
||||||
|
b.logger.LogWarn("Error disconnecting from XMPP server", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.connected.Store(false)
|
||||||
|
b.logger.LogInfo("Mattermost to XMPP bridge stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectToXMPP establishes connection to the XMPP server
|
||||||
|
func (b *xmppBridge) connectToXMPP() error {
|
||||||
|
if b.xmppClient == nil {
|
||||||
|
return fmt.Errorf("XMPP client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.LogDebug("Connecting to XMPP server")
|
||||||
|
|
||||||
|
err := b.xmppClient.Connect()
|
||||||
|
if err != nil {
|
||||||
|
b.connected.Store(false)
|
||||||
|
return fmt.Errorf("failed to connect to XMPP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.connected.Store(true)
|
||||||
|
b.logger.LogInfo("Successfully connected to XMPP server")
|
||||||
|
|
||||||
|
// Set online presence after successful connection
|
||||||
|
if err := b.xmppClient.SetOnlinePresence(); err != nil {
|
||||||
|
b.logger.LogWarn("Failed to set online presence", "error", err)
|
||||||
|
// Don't fail the connection for presence issues
|
||||||
|
} else {
|
||||||
|
b.logger.LogDebug("Set bridge user online presence")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadAndJoinMappedChannels loads channel mappings and joins corresponding XMPP rooms
|
||||||
|
func (b *xmppBridge) loadAndJoinMappedChannels() error {
|
||||||
|
b.logger.LogDebug("Loading and joining mapped channels")
|
||||||
|
|
||||||
|
// Get all channel mappings from KV store
|
||||||
|
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, no rooms to join")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.LogInfo("Found channel mappings, joining XMPP rooms", "count", len(mappings))
|
||||||
|
|
||||||
|
// Join each mapped room
|
||||||
|
for channelID, roomJID := range mappings {
|
||||||
|
if err := b.joinXMPPRoom(channelID, roomJID); err != nil {
|
||||||
|
b.logger.LogWarn("Failed to join room", "channel_id", channelID, "room_jid", roomJID, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinXMPPRoom joins an XMPP room and updates the local cache
|
||||||
|
func (b *xmppBridge) joinXMPPRoom(channelID, roomJID string) error {
|
||||||
|
if !b.connected.Load() {
|
||||||
|
return fmt.Errorf("not connected to XMPP server")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := b.xmppClient.JoinRoom(roomJID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to join XMPP room: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.LogInfo("Joined XMPP room", "channel_id", channelID, "room_jid", roomJID)
|
||||||
|
|
||||||
|
// Update local cache
|
||||||
|
b.mappingsMu.Lock()
|
||||||
|
b.channelMappings[channelID] = roomJID
|
||||||
|
b.mappingsMu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAllChannelMappings retrieves all channel mappings from KV store
|
||||||
|
func (b *xmppBridge) 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 channel mapping prefix
|
||||||
|
keys, err := b.kvstore.ListKeysWithPrefix(0, 1000, kvstore.KeyPrefixChannelMapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list channel mapping keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load each mapping
|
||||||
|
for _, key := range keys {
|
||||||
|
roomJIDBytes, err := b.kvstore.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
b.logger.LogWarn("Failed to load mapping for key", "key", key, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract channel ID from the key
|
||||||
|
channelID := kvstore.ExtractChannelIDFromKey(key)
|
||||||
|
if channelID == "" {
|
||||||
|
b.logger.LogWarn("Failed to extract channel ID from key", "key", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings[channelID] = string(roomJIDBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectionMonitor monitors the XMPP connection
|
||||||
|
func (b *xmppBridge) connectionMonitor() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-b.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := b.checkConnection(); err != nil {
|
||||||
|
b.logger.LogWarn("XMPP connection check failed", "error", err)
|
||||||
|
b.handleReconnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConnection verifies the XMPP connection is still active
|
||||||
|
func (b *xmppBridge) checkConnection() error {
|
||||||
|
if !b.connected.Load() {
|
||||||
|
return fmt.Errorf("not connected")
|
||||||
|
}
|
||||||
|
return b.xmppClient.TestConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleReconnection attempts to reconnect to XMPP and rejoin rooms
|
||||||
|
func (b *xmppBridge) handleReconnection() {
|
||||||
|
b.configMu.RLock()
|
||||||
|
config := b.config
|
||||||
|
b.configMu.RUnlock()
|
||||||
|
|
||||||
|
if config == nil || !config.EnableSync {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.LogInfo("Attempting to reconnect to XMPP server")
|
||||||
|
b.connected.Store(false)
|
||||||
|
|
||||||
|
if b.xmppClient != nil {
|
||||||
|
b.xmppClient.Disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry connection with exponential backoff
|
||||||
|
maxRetries := 3
|
||||||
|
for i := range maxRetries {
|
||||||
|
backoff := time.Duration(1<<uint(i)) * time.Second
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-b.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(backoff):
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.connectToXMPP(); err != nil {
|
||||||
|
b.logger.LogWarn("Reconnection attempt failed", "attempt", i+1, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.loadAndJoinMappedChannels(); err != nil {
|
||||||
|
b.logger.LogWarn("Failed to rejoin rooms after reconnection", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.LogInfo("Successfully reconnected to XMPP server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.LogError("Failed to reconnect to XMPP server after all attempts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API methods
|
||||||
|
|
||||||
|
// IsConnected returns whether the bridge is connected to XMPP
|
||||||
|
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 {
|
||||||
|
if b.kvstore == nil {
|
||||||
|
return fmt.Errorf("KV store not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store forward and reverse mappings
|
||||||
|
err := b.kvstore.Set(kvstore.BuildChannelMappingKey(channelID), []byte(roomJID))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to store channel room mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.kvstore.Set(kvstore.BuildRoomMappingKey(roomJID), []byte(channelID))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to store reverse room mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local cache
|
||||||
|
b.mappingsMu.Lock()
|
||||||
|
b.channelMappings[channelID] = roomJID
|
||||||
|
b.mappingsMu.Unlock()
|
||||||
|
|
||||||
|
// Join the room if connected
|
||||||
|
if b.connected.Load() {
|
||||||
|
if err := b.xmppClient.JoinRoom(roomJID); err != nil {
|
||||||
|
b.logger.LogWarn("Failed to join newly mapped room", "channel_id", channelID, "room_jid", roomJID, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.LogInfo("Created channel room mapping", "channel_id", channelID, "room_jid", roomJID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelRoomMapping gets the XMPP room JID for a Mattermost channel
|
||||||
|
func (b *xmppBridge) GetChannelRoomMapping(channelID string) (string, error) {
|
||||||
|
// Check cache first
|
||||||
|
b.mappingsMu.RLock()
|
||||||
|
roomJID, exists := b.channelMappings[channelID]
|
||||||
|
b.mappingsMu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return roomJID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.kvstore == nil {
|
||||||
|
return "", fmt.Errorf("KV store not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load from KV store
|
||||||
|
roomJIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMappingKey(channelID))
|
||||||
|
if err != nil {
|
||||||
|
return "", nil // Unmapped channels are expected
|
||||||
|
}
|
||||||
|
|
||||||
|
roomJID = string(roomJIDBytes)
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
b.mappingsMu.Lock()
|
||||||
|
b.channelMappings[channelID] = roomJID
|
||||||
|
b.mappingsMu.Unlock()
|
||||||
|
|
||||||
|
return roomJID, nil
|
||||||
}
|
}
|
|
@ -10,8 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
client *pluginapi.Client
|
client *pluginapi.Client
|
||||||
bridge pluginModel.Bridge
|
bridgeManager pluginModel.BridgeManager
|
||||||
}
|
}
|
||||||
|
|
||||||
type Command interface {
|
type Command interface {
|
||||||
|
@ -22,7 +22,7 @@ type Command interface {
|
||||||
const xmppBridgeCommandTrigger = "xmppbridge"
|
const xmppBridgeCommandTrigger = "xmppbridge"
|
||||||
|
|
||||||
// Register all your slash commands in the NewCommandHandler function.
|
// Register all your slash commands in the NewCommandHandler function.
|
||||||
func NewCommandHandler(client *pluginapi.Client, bridge pluginModel.Bridge) Command {
|
func NewCommandHandler(client *pluginapi.Client, bridgeManager pluginModel.BridgeManager) Command {
|
||||||
// Register XMPP bridge command
|
// Register XMPP bridge command
|
||||||
xmppBridgeData := model.NewAutocompleteData(xmppBridgeCommandTrigger, "", "Manage XMPP bridge")
|
xmppBridgeData := model.NewAutocompleteData(xmppBridgeCommandTrigger, "", "Manage XMPP bridge")
|
||||||
mapSubcommand := model.NewAutocompleteData("map", "[room_jid]", "Map current channel to XMPP room")
|
mapSubcommand := model.NewAutocompleteData("map", "[room_jid]", "Map current channel to XMPP room")
|
||||||
|
@ -44,8 +44,8 @@ func NewCommandHandler(client *pluginapi.Client, bridge pluginModel.Bridge) Comm
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
client: client,
|
client: client,
|
||||||
bridge: bridge,
|
bridgeManager: bridgeManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,8 +112,17 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the XMPP bridge
|
||||||
|
bridge, err := c.bridgeManager.GetBridge("xmpp")
|
||||||
|
if err != nil {
|
||||||
|
return &model.CommandResponse{
|
||||||
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
|
Text: "❌ XMPP bridge is not available. Please check the plugin configuration.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if bridge is connected
|
// Check if bridge is connected
|
||||||
if !c.bridge.IsConnected() {
|
if !bridge.IsConnected() {
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
Text: "❌ XMPP bridge is not connected. Please check the plugin configuration.",
|
Text: "❌ XMPP bridge is not connected. Please check the plugin configuration.",
|
||||||
|
@ -121,7 +130,7 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if channel is already mapped
|
// Check if channel is already mapped
|
||||||
existingMapping, err := c.bridge.GetChannelRoomMapping(channelID)
|
existingMapping, err := bridge.GetChannelRoomMapping(channelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
|
@ -137,7 +146,7 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the mapping
|
// Create the mapping
|
||||||
err = c.bridge.CreateChannelRoomMapping(channelID, roomJID)
|
err = bridge.CreateChannelRoomMapping(channelID, roomJID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
|
@ -152,7 +161,16 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandResponse {
|
func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandResponse {
|
||||||
isConnected := c.bridge.IsConnected()
|
// Get the XMPP bridge
|
||||||
|
bridge, err := c.bridgeManager.GetBridge("xmpp")
|
||||||
|
if err != nil {
|
||||||
|
return &model.CommandResponse{
|
||||||
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
|
Text: "❌ XMPP bridge is not available. Please check the plugin configuration.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected := bridge.IsConnected()
|
||||||
|
|
||||||
var statusText string
|
var statusText string
|
||||||
if isConnected {
|
if isConnected {
|
||||||
|
@ -163,7 +181,7 @@ func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandRe
|
||||||
|
|
||||||
// Check if current channel is mapped
|
// Check if current channel is mapped
|
||||||
channelID := args.ChannelId
|
channelID := args.ChannelId
|
||||||
roomJID, err := c.bridge.GetChannelRoomMapping(channelID)
|
roomJID, err := bridge.GetChannelRoomMapping(channelID)
|
||||||
|
|
||||||
var mappingText string
|
var mappingText string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -67,10 +67,11 @@ func (p *Plugin) OnConfigurationChange() error {
|
||||||
|
|
||||||
p.setConfiguration(configuration)
|
p.setConfiguration(configuration)
|
||||||
|
|
||||||
// Update bridge configurations (only if bridges have been initialized)
|
// Update bridge configurations only if bridge manager has been initialized. This prevents a
|
||||||
if p.mattermostToXMPPBridge != nil {
|
// panic if we are called before OnActivate.
|
||||||
if err := p.mattermostToXMPPBridge.UpdateConfiguration(configuration); err != nil {
|
if p.bridgeManager != nil {
|
||||||
p.logger.LogWarn("Failed to update Mattermost to XMPP bridge configuration", "error", err)
|
if err := p.bridgeManager.OnPluginConfigurationChange(configuration); err != nil {
|
||||||
|
p.logger.LogWarn("Failed to update bridge configurations", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,62 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
|
type BridgeManager interface {
|
||||||
|
// RegisterBridge registers a bridge with the given name. Returns an error if the name is empty,
|
||||||
|
// the bridge is nil, or a bridge with the same name is already registered.
|
||||||
|
RegisterBridge(name string, bridge Bridge) error
|
||||||
|
|
||||||
|
// StartBridge starts the bridge with the given name. Returns an error if the bridge
|
||||||
|
// is not registered or fails to start.
|
||||||
|
StartBridge(name string) error
|
||||||
|
|
||||||
|
// StopBridge stops the bridge with the given name. Returns an error if the bridge
|
||||||
|
// is not registered or fails to stop.
|
||||||
|
StopBridge(name string) error
|
||||||
|
|
||||||
|
// UnregisterBridge removes the bridge with the given name from the manager.
|
||||||
|
// The bridge is stopped before removal if it's currently connected.
|
||||||
|
// Returns an error if the bridge is not registered.
|
||||||
|
UnregisterBridge(name string) error
|
||||||
|
|
||||||
|
// GetBridge retrieves the bridge instance with the given name.
|
||||||
|
// Returns an error if the bridge is not registered.
|
||||||
|
GetBridge(name string) (Bridge, error)
|
||||||
|
|
||||||
|
// ListBridges returns a list of all registered bridge names.
|
||||||
|
ListBridges() []string
|
||||||
|
|
||||||
|
// HasBridge checks if a bridge with the given name is registered.
|
||||||
|
HasBridge(name string) bool
|
||||||
|
|
||||||
|
// HasBridges checks if any bridges are currently registered.
|
||||||
|
HasBridges() bool
|
||||||
|
|
||||||
|
// Shutdown stops and unregisters all bridges. Returns an error if any bridge
|
||||||
|
// fails to stop, but continues to attempt stopping all bridges.
|
||||||
|
Shutdown() error
|
||||||
|
|
||||||
|
// OnPluginConfigurationChange propagates configuration changes to all registered bridges.
|
||||||
|
// Returns an error if any bridge fails to update its configuration, but continues to
|
||||||
|
// attempt updating all bridges.
|
||||||
|
OnPluginConfigurationChange(config any) error
|
||||||
|
}
|
||||||
|
|
||||||
type Bridge interface {
|
type Bridge interface {
|
||||||
// UpdateConfiguration updates the bridge configuration
|
// UpdateConfiguration updates the bridge configuration
|
||||||
UpdateConfiguration(config any) error
|
UpdateConfiguration(config any) error
|
||||||
|
|
||||||
// Start starts the bridge
|
// Start starts the bridge
|
||||||
Start() error
|
Start() error
|
||||||
|
|
||||||
// Stop stops the bridge
|
// Stop stops the bridge
|
||||||
Stop() error
|
Stop() error
|
||||||
|
|
||||||
// CreateChannelRoomMapping creates a mapping between a Mattermost channel ID and an bridge room ID.
|
// CreateChannelRoomMapping creates a mapping between a Mattermost channel ID and an bridge room ID.
|
||||||
CreateChannelRoomMapping(channelID, roomJID string) error
|
CreateChannelRoomMapping(channelID, roomJID string) error
|
||||||
|
|
||||||
// GetChannelRoomMapping retrieves the bridge room ID for a given Mattermost channel ID.
|
// GetChannelRoomMapping retrieves the bridge room ID for a given Mattermost channel ID.
|
||||||
GetChannelRoomMapping(channelID string) (string, error)
|
GetChannelRoomMapping(channelID string) (string, error)
|
||||||
|
|
||||||
// IsConnected checks if the bridge is connected to the remote service.
|
// IsConnected checks if the bridge is connected to the remote service.
|
||||||
IsConnected() bool
|
IsConnected() bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
mattermostbridge "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge/mattermost"
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge"
|
||||||
|
xmppbridge "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/bridge/xmpp"
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/command"
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/command"
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config"
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/config"
|
||||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
||||||
|
@ -50,9 +52,8 @@ type Plugin struct {
|
||||||
// setConfiguration for usage.
|
// setConfiguration for usage.
|
||||||
configuration *config.Configuration
|
configuration *config.Configuration
|
||||||
|
|
||||||
// Bridge components for dependency injection architecture
|
// Bridge manager for managing all bridge instances
|
||||||
mattermostToXMPPBridge pluginModel.Bridge
|
bridgeManager pluginModel.BridgeManager
|
||||||
xmppToMattermostBridge pluginModel.Bridge
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnActivate is invoked when the plugin is activated. If an error is returned, the plugin will be deactivated.
|
// OnActivate is invoked when the plugin is activated. If an error is returned, the plugin will be deactivated.
|
||||||
|
@ -67,21 +68,25 @@ func (p *Plugin) OnActivate() error {
|
||||||
p.initXMPPClient()
|
p.initXMPPClient()
|
||||||
|
|
||||||
// Load configuration directly
|
// Load configuration directly
|
||||||
cfg := new(config.Configuration)
|
cfg := p.getConfiguration()
|
||||||
if err := p.API.LoadPluginConfiguration(cfg); err != nil {
|
|
||||||
p.logger.LogWarn("Failed to load plugin configuration during activation", "error", err)
|
|
||||||
cfg = &config.Configuration{} // Use empty config as fallback
|
|
||||||
}
|
|
||||||
p.logger.LogDebug("Loaded configuration in OnActivate", "config", cfg)
|
p.logger.LogDebug("Loaded configuration in OnActivate", "config", cfg)
|
||||||
|
|
||||||
// Initialize bridges with current configuration
|
// Initialize bridge manager
|
||||||
p.initBridges(*cfg)
|
p.bridgeManager = bridge.NewManager(p.logger)
|
||||||
|
|
||||||
p.commandClient = command.NewCommandHandler(p.client, p.mattermostToXMPPBridge)
|
// Initialize and register bridges with current configuration
|
||||||
|
if err := p.initBridges(*cfg); err != nil {
|
||||||
|
p.logger.LogError("Failed to initialize bridges", "error", err)
|
||||||
|
return fmt.Errorf("failed to initialize bridges: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Start the bridge
|
p.commandClient = command.NewCommandHandler(p.client, p.bridgeManager)
|
||||||
if err := p.mattermostToXMPPBridge.Start(); err != nil {
|
|
||||||
p.logger.LogWarn("Failed to start bridge during activation", "error", err)
|
// Start all bridges
|
||||||
|
for _, bridgeName := range p.bridgeManager.ListBridges() {
|
||||||
|
if err := p.bridgeManager.StartBridge(bridgeName); err != nil {
|
||||||
|
p.logger.LogWarn("Failed to start bridge during activation", "bridge", bridgeName, "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
job, err := cluster.Schedule(
|
job, err := cluster.Schedule(
|
||||||
|
@ -107,8 +112,10 @@ func (p *Plugin) OnDeactivate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.mattermostToXMPPBridge.Stop(); err != nil {
|
if p.bridgeManager != nil {
|
||||||
p.API.LogError("Failed to stop Mattermost to XMPP bridge", "err", err)
|
if err := p.bridgeManager.Shutdown(); err != nil {
|
||||||
|
p.API.LogError("Failed to shutdown bridge manager", "err", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -134,22 +141,21 @@ func (p *Plugin) initXMPPClient() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) initBridges(cfg config.Configuration) {
|
func (p *Plugin) initBridges(cfg config.Configuration) error {
|
||||||
if p.mattermostToXMPPBridge == nil {
|
// Create and register XMPP bridge
|
||||||
// Create bridge instances with all dependencies and configuration
|
bridge := xmppbridge.NewBridge(
|
||||||
p.mattermostToXMPPBridge = mattermostbridge.NewMattermostToXMPPBridge(
|
p.logger,
|
||||||
p.logger,
|
p.API,
|
||||||
p.API,
|
p.kvstore,
|
||||||
p.kvstore,
|
&cfg,
|
||||||
&cfg,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
p.logger.LogInfo("Bridge instances created successfully")
|
if err := p.bridgeManager.RegisterBridge("xmpp", bridge); err != nil {
|
||||||
|
return fmt.Errorf("failed to register XMPP bridge: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if p.xmppToMattermostBridge == nil {
|
p.logger.LogInfo("Bridge instances created and registered successfully")
|
||||||
// p.xmppToMattermostBridge = xmppbridge.NewXMPPToMattermostBridge()
|
return nil
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://developers.mattermost.com/extend/plugins/server/reference/
|
// See https://developers.mattermost.com/extend/plugins/server/reference/
|
||||||
|
|
|
@ -77,3 +77,11 @@ func BuildXMPPEventPostKey(xmppEventID string) string {
|
||||||
func BuildXMPPReactionKey(reactionEventID string) string {
|
func BuildXMPPReactionKey(reactionEventID string) string {
|
||||||
return KeyPrefixXMPPReaction + reactionEventID
|
return KeyPrefixXMPPReaction + reactionEventID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractChannelIDFromKey extracts the channel ID from a channel mapping key
|
||||||
|
func ExtractChannelIDFromKey(key string) string {
|
||||||
|
if len(key) <= len(KeyPrefixChannelMapping) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return key[len(KeyPrefixChannelMapping):]
|
||||||
|
}
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"mellium.im/sasl"
|
"mellium.im/sasl"
|
||||||
"mellium.im/xmpp"
|
"mellium.im/xmpp"
|
||||||
"mellium.im/xmpp/jid"
|
"mellium.im/xmpp/jid"
|
||||||
|
"mellium.im/xmpp/muc"
|
||||||
|
"mellium.im/xmpp/mux"
|
||||||
"mellium.im/xmpp/stanza"
|
"mellium.im/xmpp/stanza"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents an XMPP client for communicating with XMPP servers.
|
// Client represents an XMPP client for communicating with XMPP servers.
|
||||||
|
@ -26,10 +26,14 @@ type Client struct {
|
||||||
tlsConfig *tls.Config // custom TLS configuration
|
tlsConfig *tls.Config // custom TLS configuration
|
||||||
|
|
||||||
// XMPP connection
|
// XMPP connection
|
||||||
session *xmpp.Session
|
session *xmpp.Session
|
||||||
jidAddr jid.JID
|
jidAddr jid.JID
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
mucClient *muc.Client
|
||||||
|
mux *mux.ServeMux
|
||||||
|
sessionReady chan struct{}
|
||||||
|
sessionServing bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageRequest represents a request to send a message.
|
// MessageRequest represents a request to send a message.
|
||||||
|
@ -62,14 +66,20 @@ type UserProfile struct {
|
||||||
// NewClient creates a new XMPP client.
|
// NewClient creates a new XMPP client.
|
||||||
func NewClient(serverURL, username, password, resource, remoteID string) *Client {
|
func NewClient(serverURL, username, password, resource, remoteID string) *Client {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
mucClient := &muc.Client{}
|
||||||
|
mux := mux.New("jabber:client", muc.HandleClient(mucClient))
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
serverURL: serverURL,
|
serverURL: serverURL,
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
resource: resource,
|
resource: resource,
|
||||||
remoteID: remoteID,
|
remoteID: remoteID,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
mucClient: mucClient,
|
||||||
|
mux: mux,
|
||||||
|
sessionReady: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,18 +101,22 @@ func (c *Client) Connect() error {
|
||||||
return nil // Already connected
|
return nil // Already connected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset session ready channel for reconnection
|
||||||
|
c.sessionReady = make(chan struct{})
|
||||||
|
c.sessionServing = false
|
||||||
|
|
||||||
// Parse JID
|
// Parse JID
|
||||||
var err error
|
var err error
|
||||||
c.jidAddr, err = jid.Parse(c.username)
|
c.jidAddr, err = jid.Parse(c.username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to parse username as JID")
|
return fmt.Errorf("failed to parse username as JID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add resource if not present
|
// Add resource if not present
|
||||||
if c.jidAddr.Resourcepart() == "" {
|
if c.jidAddr.Resourcepart() == "" {
|
||||||
c.jidAddr, err = c.jidAddr.WithResource(c.resource)
|
c.jidAddr, err = c.jidAddr.WithResource(c.resource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to add resource to JID")
|
return fmt.Errorf("failed to add resource to JID: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,10 +139,49 @@ func (c *Client) Connect() error {
|
||||||
xmpp.BindResource(),
|
xmpp.BindResource(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to establish XMPP session")
|
return fmt.Errorf("failed to establish XMPP session: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Start serving the session with the multiplexer to handle incoming stanzas
|
||||||
|
go c.serveSession()
|
||||||
|
|
||||||
|
// Wait for the session to be ready with a timeout
|
||||||
|
select {
|
||||||
|
case <-c.sessionReady:
|
||||||
|
if !c.sessionServing {
|
||||||
|
return fmt.Errorf("failed to start session serving")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
return fmt.Errorf("timeout waiting for session to be ready")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveSession handles incoming XMPP stanzas through the multiplexer
|
||||||
|
func (c *Client) serveSession() {
|
||||||
|
if c.session == nil || c.mux == nil {
|
||||||
|
close(c.sessionReady) // Signal failure
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal that the session is ready to serve
|
||||||
|
c.sessionServing = true
|
||||||
|
close(c.sessionReady)
|
||||||
|
|
||||||
|
err := c.session.Serve(c.mux)
|
||||||
|
if err != nil {
|
||||||
|
c.sessionServing = false
|
||||||
|
// Handle session serve errors
|
||||||
|
// In production, you might want to log this error or attempt reconnection
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
// Context cancelled, normal shutdown
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Unexpected error during session serve
|
||||||
|
// Could trigger reconnection logic here
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect closes the XMPP connection
|
// Disconnect closes the XMPP connection
|
||||||
|
@ -137,7 +190,7 @@ func (c *Client) Disconnect() error {
|
||||||
err := c.session.Close()
|
err := c.session.Close()
|
||||||
c.session = nil
|
c.session = nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to close XMPP session")
|
return fmt.Errorf("failed to close XMPP session: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +212,7 @@ func (c *Client) TestConnection() error {
|
||||||
// For now, just check if session exists and is not closed
|
// For now, just check if session exists and is not closed
|
||||||
// A proper ping implementation would require more complex IQ handling
|
// A proper ping implementation would require more complex IQ handling
|
||||||
if c.session == nil {
|
if c.session == nil {
|
||||||
return errors.New("XMPP session is not established")
|
return fmt.Errorf("XMPP session is not established")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -173,14 +226,83 @@ func (c *Client) JoinRoom(roomJID string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
room, err := jid.Parse(roomJID)
|
if c.mucClient == nil {
|
||||||
if err != nil {
|
return fmt.Errorf("MUC client not initialized")
|
||||||
return errors.Wrap(err, "failed to parse room JID")
|
}
|
||||||
|
|
||||||
|
room, err := jid.Parse(roomJID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse room JID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use our username as nickname
|
||||||
|
nickname := c.jidAddr.Localpart()
|
||||||
|
roomWithNickname, err := room.WithResource(nickname)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add nickname to room JID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a context with timeout for the join operation
|
||||||
|
joinCtx, cancel := context.WithTimeout(c.ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Join the MUC room using the proper MUC client with timeout
|
||||||
|
opts := []muc.Option{
|
||||||
|
muc.MaxBytes(0), // Don't limit message history
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the join operation in a goroutine to avoid blocking
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := c.mucClient.Join(joinCtx, roomWithNickname, c.session, opts...)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for join to complete or timeout
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to join MUC room: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case <-joinCtx.Done():
|
||||||
|
return fmt.Errorf("timeout joining MUC room %s", roomJID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeaveRoom leaves an XMPP Multi-User Chat room
|
||||||
|
func (c *Client) LeaveRoom(roomJID string) error {
|
||||||
|
if c.session == nil {
|
||||||
|
return fmt.Errorf("XMPP session not established")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.mucClient == nil {
|
||||||
|
return fmt.Errorf("MUC client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
room, err := jid.Parse(roomJID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse room JID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use our username as nickname
|
||||||
|
nickname := c.jidAddr.Localpart()
|
||||||
|
roomWithNickname, err := room.WithResource(nickname)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add nickname to room JID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send unavailable presence to leave the room
|
||||||
|
presence := stanza.Presence{
|
||||||
|
From: c.jidAddr,
|
||||||
|
To: roomWithNickname,
|
||||||
|
Type: stanza.UnavailablePresence,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.session.Encode(c.ctx, presence); err != nil {
|
||||||
|
return fmt.Errorf("failed to send leave presence to MUC room: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, just store that we would join the room
|
|
||||||
// Proper MUC implementation would require more complex presence handling
|
|
||||||
_ = room
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +316,7 @@ func (c *Client) SendMessage(req MessageRequest) (*SendMessageResponse, error) {
|
||||||
|
|
||||||
to, err := jid.Parse(req.RoomJID)
|
to, err := jid.Parse(req.RoomJID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to parse destination JID")
|
return nil, fmt.Errorf("failed to parse destination JID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create message stanza
|
// Create message stanza
|
||||||
|
@ -216,30 +338,13 @@ func (c *Client) SendMessage(req MessageRequest) (*SendMessageResponse, error) {
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateGhostUser creates a ghost user representation
|
|
||||||
func (c *Client) CreateGhostUser(mattermostUserID, displayName string, avatarData []byte, avatarContentType string) (*GhostUser, error) {
|
|
||||||
domain := c.jidAddr.Domain().String()
|
|
||||||
if c.serverDomain != "" {
|
|
||||||
domain = c.serverDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
ghostJID := fmt.Sprintf("mattermost_%s@%s", mattermostUserID, domain)
|
|
||||||
|
|
||||||
ghost := &GhostUser{
|
|
||||||
JID: ghostJID,
|
|
||||||
DisplayName: displayName,
|
|
||||||
}
|
|
||||||
|
|
||||||
return ghost, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveRoomAlias resolves a room alias to room JID
|
// ResolveRoomAlias resolves a room alias to room JID
|
||||||
func (c *Client) ResolveRoomAlias(roomAlias string) (string, error) {
|
func (c *Client) ResolveRoomAlias(roomAlias string) (string, error) {
|
||||||
// For XMPP, return the alias as-is if it's already a valid JID
|
// For XMPP, return the alias as-is if it's already a valid JID
|
||||||
if _, err := jid.Parse(roomAlias); err == nil {
|
if _, err := jid.Parse(roomAlias); err == nil {
|
||||||
return roomAlias, nil
|
return roomAlias, nil
|
||||||
}
|
}
|
||||||
return "", errors.New("invalid room alias/JID")
|
return "", fmt.Errorf("invalid room alias/JID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserProfile gets user profile information
|
// GetUserProfile gets user profile information
|
||||||
|
@ -254,7 +359,7 @@ func (c *Client) GetUserProfile(userJID string) (*UserProfile, error) {
|
||||||
// SetOnlinePresence sends an online presence stanza to indicate the client is available
|
// SetOnlinePresence sends an online presence stanza to indicate the client is available
|
||||||
func (c *Client) SetOnlinePresence() error {
|
func (c *Client) SetOnlinePresence() error {
|
||||||
if c.session == nil {
|
if c.session == nil {
|
||||||
return errors.New("XMPP session not established")
|
return fmt.Errorf("XMPP session not established")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create presence stanza indicating we're available
|
// Create presence stanza indicating we're available
|
||||||
|
@ -265,7 +370,7 @@ func (c *Client) SetOnlinePresence() error {
|
||||||
|
|
||||||
// Send the presence stanza
|
// Send the presence stanza
|
||||||
if err := c.session.Encode(c.ctx, presence); err != nil {
|
if err := c.session.Encode(c.ctx, presence); err != nil {
|
||||||
return errors.Wrap(err, "failed to send online presence")
|
return fmt.Errorf("failed to send online presence: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -74,7 +74,26 @@ After completing the setup wizard:
|
||||||
- **Email**: `testuser@localhost`
|
- **Email**: `testuser@localhost`
|
||||||
4. Click **Create User**
|
4. Click **Create User**
|
||||||
|
|
||||||
### 4. Test Connectivity
|
### 4. Create Test MUC Room
|
||||||
|
|
||||||
|
For testing Multi-User Chat functionality, create a test room:
|
||||||
|
|
||||||
|
1. In the admin console, go to **Group Chat** → **Create New Room**
|
||||||
|
2. Fill in the room details:
|
||||||
|
- **Room ID**: `test1`
|
||||||
|
- **Room Name**: `Test Room 1`
|
||||||
|
- **Description**: `Test room for XMPP bridge development`
|
||||||
|
- **Subject**: `Development Test Room`
|
||||||
|
3. Configure room settings:
|
||||||
|
- **Room Type**: Public (searchable and accessible)
|
||||||
|
- **Persistent**: Yes (room survives server restarts)
|
||||||
|
- **Max occupants**: 50 (or leave default)
|
||||||
|
- **Enable**: Yes
|
||||||
|
4. Click **Create Room**
|
||||||
|
|
||||||
|
The room will be accessible as `test1@conference.localhost` for testing MUC operations.
|
||||||
|
|
||||||
|
### 5. Test Connectivity
|
||||||
|
|
||||||
Run the doctor tool to verify everything is working:
|
Run the doctor tool to verify everything is working:
|
||||||
|
|
||||||
|
@ -84,6 +103,17 @@ make devserver_doctor
|
||||||
|
|
||||||
You should see successful connection, ping, and disconnect messages.
|
You should see successful connection, ping, and disconnect messages.
|
||||||
|
|
||||||
|
#### Test MUC Operations
|
||||||
|
|
||||||
|
To test Multi-User Chat room operations (requires the test room created above):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test MUC room join/leave operations
|
||||||
|
go run cmd/xmpp-client-doctor/main.go --test-muc
|
||||||
|
```
|
||||||
|
|
||||||
|
This will test joining the `test1@conference.localhost` room, waiting 5 seconds, and then leaving.
|
||||||
|
|
||||||
## Server Details
|
## Server Details
|
||||||
|
|
||||||
- **Admin Console**: http://localhost:9090
|
- **Admin Console**: http://localhost:9090
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue