mattermost-plugin-bridge-xmpp/server/bridge/xmpp/user.go
Felipe Martin 65038fb7a2
Some checks are pending
ci / plugin-ci (push) Waiting to run
feat: restore XMPP bridge to use direct client connection instead of bridge user
- Replace bridgeUser with bridgeClient (*xmppClient.Client) in XMPP bridge
- Update createXMPPClient to return XMPP client with TLS configuration
- Migrate connection, disconnection, and room operations to use bridgeClient
- Update Ping() and RoomExists() methods to use client methods directly
- Maintain bridge-agnostic user management system for additional users
- Fix formatting and import organization across bridge components

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-04 18:04:10 +02:00

336 lines
8.3 KiB
Go

package xmpp
import (
"context"
"crypto/tls"
"fmt"
"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/model"
xmppClient "github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp"
)
// XMPPUser represents an XMPP user that implements the BridgeUser interface
type XMPPUser struct {
// User identity
id string
displayName string
jid string
// XMPP client
client *xmppClient.Client
// State management
state model.UserState
stateMu sync.RWMutex
connected atomic.Bool
// Configuration
config *config.Configuration
// Goroutine lifecycle
ctx context.Context
cancel context.CancelFunc
// Logger
logger logger.Logger
}
// NewXMPPUser creates a new XMPP user
func NewXMPPUser(id, displayName, jid string, cfg *config.Configuration, logger logger.Logger) *XMPPUser {
ctx, cancel := context.WithCancel(context.Background())
// Create TLS config based on certificate verification setting
tlsConfig := &tls.Config{
InsecureSkipVerify: cfg.XMPPInsecureSkipVerify,
}
// Create XMPP client for this user
client := xmppClient.NewClientWithTLS(
cfg.XMPPServerURL,
jid,
cfg.XMPPPassword, // This might need to be user-specific in the future
cfg.GetXMPPResource(),
id, // Use user ID as remote ID
tlsConfig,
logger,
)
return &XMPPUser{
id: id,
displayName: displayName,
jid: jid,
client: client,
state: model.UserStateOffline,
config: cfg,
ctx: ctx,
cancel: cancel,
logger: logger,
}
}
// Validation
func (u *XMPPUser) Validate() error {
if u.id == "" {
return fmt.Errorf("user ID cannot be empty")
}
if u.jid == "" {
return fmt.Errorf("JID cannot be empty")
}
if u.config == nil {
return fmt.Errorf("configuration cannot be nil")
}
if u.config.XMPPServerURL == "" {
return fmt.Errorf("XMPP server URL cannot be empty")
}
if u.client == nil {
return fmt.Errorf("XMPP client cannot be nil")
}
return nil
}
// Identity (bridge-agnostic)
func (u *XMPPUser) GetID() string {
return u.id
}
func (u *XMPPUser) GetDisplayName() string {
return u.displayName
}
// State management
func (u *XMPPUser) GetState() model.UserState {
u.stateMu.RLock()
defer u.stateMu.RUnlock()
return u.state
}
func (u *XMPPUser) SetState(state model.UserState) error {
u.stateMu.Lock()
defer u.stateMu.Unlock()
u.logger.LogDebug("Changing XMPP user state", "user_id", u.id, "old_state", u.state, "new_state", state)
u.state = state
// TODO: Send presence update to XMPP server based on state
// This would involve mapping UserState to XMPP presence types
return nil
}
// Channel operations
func (u *XMPPUser) JoinChannel(channelID string) error {
if !u.connected.Load() {
return fmt.Errorf("user %s is not connected", u.id)
}
u.logger.LogDebug("XMPP user joining channel", "user_id", u.id, "channel_id", channelID)
// For XMPP, channelID is the room JID
err := u.client.JoinRoom(channelID)
if err != nil {
return fmt.Errorf("failed to join XMPP room %s: %w", channelID, err)
}
u.logger.LogInfo("XMPP user joined channel", "user_id", u.id, "channel_id", channelID)
return nil
}
func (u *XMPPUser) LeaveChannel(channelID string) error {
if !u.connected.Load() {
return fmt.Errorf("user %s is not connected", u.id)
}
u.logger.LogDebug("XMPP user leaving channel", "user_id", u.id, "channel_id", channelID)
// For XMPP, channelID is the room JID
err := u.client.LeaveRoom(channelID)
if err != nil {
return fmt.Errorf("failed to leave XMPP room %s: %w", channelID, err)
}
u.logger.LogInfo("XMPP user left channel", "user_id", u.id, "channel_id", channelID)
return nil
}
func (u *XMPPUser) SendMessageToChannel(channelID, message string) error {
if !u.connected.Load() {
return fmt.Errorf("user %s is not connected", u.id)
}
u.logger.LogDebug("XMPP user sending message to channel", "user_id", u.id, "channel_id", channelID)
// Create message request for XMPP
req := xmppClient.MessageRequest{
RoomJID: channelID,
GhostUserJID: u.jid,
Message: message,
}
_, err := u.client.SendMessage(req)
if err != nil {
return fmt.Errorf("failed to send message to XMPP room %s: %w", channelID, err)
}
u.logger.LogDebug("XMPP user sent message to channel", "user_id", u.id, "channel_id", channelID)
return nil
}
// Connection lifecycle
func (u *XMPPUser) Connect() error {
u.logger.LogDebug("Connecting XMPP user", "user_id", u.id, "jid", u.jid)
err := u.client.Connect()
if err != nil {
u.connected.Store(false)
return fmt.Errorf("failed to connect XMPP user %s: %w", u.id, err)
}
u.connected.Store(true)
u.logger.LogInfo("XMPP user connected", "user_id", u.id, "jid", u.jid)
// Set online presence after successful connection
if err := u.client.SetOnlinePresence(); err != nil {
u.logger.LogWarn("Failed to set online presence for XMPP user", "user_id", u.id, "error", err)
// Don't fail the connection for presence issues
}
// Update state to online
_ = u.SetState(model.UserStateOnline)
return nil
}
func (u *XMPPUser) Disconnect() error {
u.logger.LogDebug("Disconnecting XMPP user", "user_id", u.id, "jid", u.jid)
if u.client == nil {
return nil
}
err := u.client.Disconnect()
if err != nil {
u.logger.LogWarn("Error disconnecting XMPP user", "user_id", u.id, "error", err)
}
u.connected.Store(false)
_ = u.SetState(model.UserStateOffline)
u.logger.LogInfo("XMPP user disconnected", "user_id", u.id, "jid", u.jid)
return err
}
func (u *XMPPUser) IsConnected() bool {
return u.connected.Load()
}
func (u *XMPPUser) Ping() error {
if !u.connected.Load() {
return fmt.Errorf("XMPP user %s is not connected", u.id)
}
if u.client == nil {
return fmt.Errorf("XMPP client not initialized for user %s", u.id)
}
return u.client.Ping()
}
// CheckChannelExists checks if an XMPP room/channel exists
func (u *XMPPUser) CheckChannelExists(channelID string) (bool, error) {
if !u.connected.Load() {
return false, fmt.Errorf("XMPP user %s is not connected", u.id)
}
if u.client == nil {
return false, fmt.Errorf("XMPP client not initialized for user %s", u.id)
}
return u.client.CheckRoomExists(channelID)
}
// Goroutine lifecycle
func (u *XMPPUser) Start(ctx context.Context) error {
u.logger.LogDebug("Starting XMPP user", "user_id", u.id, "jid", u.jid)
// Update context
u.ctx = ctx
// Connect to XMPP server
if err := u.Connect(); err != nil {
return fmt.Errorf("failed to start XMPP user %s: %w", u.id, err)
}
// Start connection monitoring in a goroutine
go u.connectionMonitor()
u.logger.LogInfo("XMPP user started", "user_id", u.id, "jid", u.jid)
return nil
}
func (u *XMPPUser) Stop() error {
u.logger.LogDebug("Stopping XMPP user", "user_id", u.id, "jid", u.jid)
// Cancel context to stop goroutines
if u.cancel != nil {
u.cancel()
}
// Disconnect from XMPP server
if err := u.Disconnect(); err != nil {
u.logger.LogWarn("Error disconnecting XMPP user during stop", "user_id", u.id, "error", err)
}
u.logger.LogInfo("XMPP user stopped", "user_id", u.id, "jid", u.jid)
return nil
}
// connectionMonitor monitors the XMPP connection for this user
func (u *XMPPUser) connectionMonitor() {
u.logger.LogDebug("Starting connection monitor for XMPP user", "user_id", u.id)
// Simple monitoring - check connection periodically
for {
select {
case <-u.ctx.Done():
u.logger.LogDebug("Connection monitor stopped for XMPP user", "user_id", u.id)
return
default:
// Check connection every 30 seconds
if u.connected.Load() {
if err := u.client.Ping(); err != nil {
u.logger.LogWarn("Connection check failed for XMPP user", "user_id", u.id, "error", err)
u.connected.Store(false)
_ = u.SetState(model.UserStateOffline)
// TODO: Implement reconnection logic if needed
}
}
// Wait before next check
timeoutCtx, cancel := context.WithTimeout(u.ctx, 30*time.Second) // 30 seconds
select {
case <-u.ctx.Done():
cancel()
return
case <-timeoutCtx.Done():
cancel()
continue
}
}
}
}
// GetJID returns the XMPP JID for this user (XMPP-specific method)
func (u *XMPPUser) GetJID() string {
return u.jid
}
// GetClient returns the underlying XMPP client (for advanced operations)
func (u *XMPPUser) GetClient() *xmppClient.Client {
return u.client
}