- Fix configuration loading by matching JSON field names with plugin manifest keys - Move configuration to separate package to resolve type conflicts - Implement bridge startup logic that initializes on OnActivate and updates on OnConfigurationChange - Add certificate verification skip option for development/testing environments - Create XMPP client initialization helper function to avoid code duplication - Add SetOnlinePresence() method to XMPP client for presence management - Set bridge user online presence automatically upon successful XMPP connection - Remove unused mock generation and test files as requested - Update bridge constructor to accept configuration parameter - Implement proper bridge lifecycle management with Start/Stop methods The bridge now properly loads configuration from admin console, creates XMPP connections with appropriate TLS settings, and manages online presence for the bridge user. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
272 lines
6.8 KiB
Go
272 lines
6.8 KiB
Go
// Package xmpp provides XMPP client functionality for the Mattermost bridge.
|
|
package xmpp
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"time"
|
|
|
|
"mellium.im/sasl"
|
|
"mellium.im/xmpp"
|
|
"mellium.im/xmpp/jid"
|
|
"mellium.im/xmpp/stanza"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Client represents an XMPP client for communicating with XMPP servers.
|
|
type Client struct {
|
|
serverURL string
|
|
username string
|
|
password string
|
|
resource string
|
|
remoteID string // Plugin remote ID for metadata
|
|
serverDomain string // explicit server domain for testing
|
|
tlsConfig *tls.Config // custom TLS configuration
|
|
|
|
// XMPP connection
|
|
session *xmpp.Session
|
|
jidAddr jid.JID
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// MessageRequest represents a request to send a message.
|
|
type MessageRequest struct {
|
|
RoomJID string `json:"room_jid"` // Required: XMPP room JID
|
|
GhostUserJID string `json:"ghost_user_jid"` // Required: Ghost user JID to send as
|
|
Message string `json:"message"` // Required: Plain text message content
|
|
HTMLMessage string `json:"html_message"` // Optional: HTML formatted message content
|
|
ThreadID string `json:"thread_id"` // Optional: Thread ID
|
|
PostID string `json:"post_id"` // Optional: Mattermost post ID metadata
|
|
}
|
|
|
|
// SendMessageResponse represents the response from XMPP when sending messages.
|
|
type SendMessageResponse struct {
|
|
StanzaID string `json:"stanza_id"`
|
|
}
|
|
|
|
// GhostUser represents an XMPP ghost user
|
|
type GhostUser struct {
|
|
JID string `json:"jid"`
|
|
DisplayName string `json:"display_name"`
|
|
}
|
|
|
|
// UserProfile represents an XMPP user profile
|
|
type UserProfile struct {
|
|
JID string `json:"jid"`
|
|
DisplayName string `json:"display_name"`
|
|
}
|
|
|
|
// NewClient creates a new XMPP client.
|
|
func NewClient(serverURL, username, password, resource, remoteID string) *Client {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &Client{
|
|
serverURL: serverURL,
|
|
username: username,
|
|
password: password,
|
|
resource: resource,
|
|
remoteID: remoteID,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
// NewClientWithTLS creates a new XMPP client with custom TLS configuration.
|
|
func NewClientWithTLS(serverURL, username, password, resource, remoteID string, tlsConfig *tls.Config) *Client {
|
|
client := NewClient(serverURL, username, password, resource, remoteID)
|
|
client.tlsConfig = tlsConfig
|
|
return client
|
|
}
|
|
|
|
// SetServerDomain sets an explicit server domain (used for testing)
|
|
func (c *Client) SetServerDomain(domain string) {
|
|
c.serverDomain = domain
|
|
}
|
|
|
|
// Connect establishes connection to the XMPP server
|
|
func (c *Client) Connect() error {
|
|
if c.session != nil {
|
|
return nil // Already connected
|
|
}
|
|
|
|
// Parse JID
|
|
var err error
|
|
c.jidAddr, err = jid.Parse(c.username)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse username as JID")
|
|
}
|
|
|
|
// Add resource if not present
|
|
if c.jidAddr.Resourcepart() == "" {
|
|
c.jidAddr, err = c.jidAddr.WithResource(c.resource)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to add resource to JID")
|
|
}
|
|
}
|
|
|
|
// Prepare TLS configuration
|
|
var tlsConfig *tls.Config
|
|
if c.tlsConfig != nil {
|
|
tlsConfig = c.tlsConfig
|
|
} else {
|
|
tlsConfig = &tls.Config{
|
|
ServerName: c.jidAddr.Domain().String(),
|
|
}
|
|
}
|
|
|
|
// Use DialClientSession for proper SASL authentication
|
|
c.session, err = xmpp.DialClientSession(
|
|
c.ctx,
|
|
c.jidAddr,
|
|
xmpp.StartTLS(tlsConfig),
|
|
xmpp.SASL("", c.password, sasl.Plain),
|
|
xmpp.BindResource(),
|
|
)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to establish XMPP session")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Disconnect closes the XMPP connection
|
|
func (c *Client) Disconnect() error {
|
|
if c.session != nil {
|
|
err := c.session.Close()
|
|
c.session = nil
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to close XMPP session")
|
|
}
|
|
}
|
|
|
|
if c.cancel != nil {
|
|
c.cancel()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TestConnection tests the XMPP connection
|
|
func (c *Client) TestConnection() error {
|
|
if c.session == nil {
|
|
if err := c.Connect(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// For now, just check if session exists and is not closed
|
|
// A proper ping implementation would require more complex IQ handling
|
|
if c.session == nil {
|
|
return errors.New("XMPP session is not established")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// JoinRoom joins an XMPP Multi-User Chat room
|
|
func (c *Client) JoinRoom(roomJID string) error {
|
|
if c.session == nil {
|
|
if err := c.Connect(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
room, err := jid.Parse(roomJID)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse room JID")
|
|
}
|
|
|
|
// For now, just store that we would join the room
|
|
// Proper MUC implementation would require more complex presence handling
|
|
_ = room
|
|
return nil
|
|
}
|
|
|
|
// SendMessage sends a message to an XMPP room
|
|
func (c *Client) SendMessage(req MessageRequest) (*SendMessageResponse, error) {
|
|
if c.session == nil {
|
|
if err := c.Connect(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
to, err := jid.Parse(req.RoomJID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse destination JID")
|
|
}
|
|
|
|
// Create message stanza
|
|
msg := stanza.Message{
|
|
Type: stanza.GroupChatMessage,
|
|
To: to,
|
|
}
|
|
|
|
// For now, just create a simple message structure
|
|
// Proper implementation would require encoding the message body
|
|
_ = msg
|
|
_ = req.Message
|
|
|
|
// Generate a response
|
|
response := &SendMessageResponse{
|
|
StanzaID: fmt.Sprintf("msg_%d", time.Now().UnixNano()),
|
|
}
|
|
|
|
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
|
|
func (c *Client) ResolveRoomAlias(roomAlias string) (string, error) {
|
|
// For XMPP, return the alias as-is if it's already a valid JID
|
|
if _, err := jid.Parse(roomAlias); err == nil {
|
|
return roomAlias, nil
|
|
}
|
|
return "", errors.New("invalid room alias/JID")
|
|
}
|
|
|
|
// GetUserProfile gets user profile information
|
|
func (c *Client) GetUserProfile(userJID string) (*UserProfile, error) {
|
|
profile := &UserProfile{
|
|
JID: userJID,
|
|
DisplayName: userJID, // Default to JID if no display name available
|
|
}
|
|
return profile, nil
|
|
}
|
|
|
|
// SetOnlinePresence sends an online presence stanza to indicate the client is available
|
|
func (c *Client) SetOnlinePresence() error {
|
|
if c.session == nil {
|
|
return errors.New("XMPP session not established")
|
|
}
|
|
|
|
// Create presence stanza indicating we're available
|
|
presence := stanza.Presence{
|
|
Type: stanza.AvailablePresence,
|
|
From: c.jidAddr,
|
|
}
|
|
|
|
// Send the presence stanza
|
|
if err := c.session.Encode(c.ctx, presence); err != nil {
|
|
return errors.Wrap(err, "failed to send online presence")
|
|
}
|
|
|
|
return nil
|
|
}
|