feat: implement bidirectional message bridge system with XMPP-Mattermost integration
This commit implements a comprehensive bridge-agnostic message routing system that enables real-time bidirectional message synchronization between XMPP and Mattermost platforms. Key features: - Bridge-agnostic message types and structures for extensibility - Central message bus system with publisher-subscriber pattern - Complete Bridge interface implementation for both XMPP and Mattermost - Message aggregation from multiple sources for scalability - Loop prevention mechanisms to avoid infinite message cycles - Buffered channels for high-performance message processing Architecture highlights: - Producer-consumer pattern for message routing between bridges - Thread-safe goroutine lifecycle management with context cancellation - Message handlers separated into dedicated files for maintainability - Support for future bridge implementations (Slack, Discord, etc.) - Markdown content standardization across all bridges Files added: - server/model/message.go: Core bridge-agnostic message structures - server/bridge/messagebus.go: Central message routing system - server/bridge/mattermost/message_handler.go: Mattermost-specific message processing - server/bridge/xmpp/message_handler.go: XMPP-specific message processing Files modified: - server/bridge/manager.go: Integration with message bus and routing - server/bridge/mattermost/bridge.go: Complete Bridge interface implementation - server/bridge/xmpp/bridge.go: Message aggregation and interface completion - server/model/bridge.go: Extended Bridge interface for bidirectional messaging - server/xmpp/client.go: Enhanced message listening with mellium.im/xmpp 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
69a67704f4
commit
7b56cb34c6
9 changed files with 1119 additions and 41 deletions
|
@ -9,7 +9,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/model"
|
||||
"mellium.im/sasl"
|
||||
"mellium.im/xmlstream"
|
||||
"mellium.im/xmpp"
|
||||
"mellium.im/xmpp/disco"
|
||||
"mellium.im/xmpp/jid"
|
||||
|
@ -21,6 +23,9 @@ import (
|
|||
const (
|
||||
// defaultOperationTimeout is the default timeout for XMPP operations
|
||||
defaultOperationTimeout = 5 * time.Second
|
||||
|
||||
// msgBufferSize is the buffer size for incoming message channels
|
||||
msgBufferSize = 1000
|
||||
)
|
||||
|
||||
// Client represents an XMPP client for communicating with XMPP servers.
|
||||
|
@ -43,6 +48,9 @@ type Client struct {
|
|||
mux *mux.ServeMux
|
||||
sessionReady chan struct{}
|
||||
sessionServing bool
|
||||
|
||||
// Message handling for bridge integration
|
||||
incomingMessages chan *model.DirectionalMessage
|
||||
}
|
||||
|
||||
// MessageRequest represents a request to send a message.
|
||||
|
@ -90,22 +98,31 @@ type UserProfile struct {
|
|||
// NewClient creates a new XMPP client.
|
||||
func NewClient(serverURL, username, password, resource, remoteID string, logger logger.Logger) *Client {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
mucClient := &muc.Client{}
|
||||
mux := mux.New("jabber:client", muc.HandleClient(mucClient))
|
||||
|
||||
return &Client{
|
||||
serverURL: serverURL,
|
||||
username: username,
|
||||
password: password,
|
||||
resource: resource,
|
||||
remoteID: remoteID,
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
mucClient: mucClient,
|
||||
mux: mux,
|
||||
sessionReady: make(chan struct{}),
|
||||
client := &Client{
|
||||
serverURL: serverURL,
|
||||
username: username,
|
||||
password: password,
|
||||
resource: resource,
|
||||
remoteID: remoteID,
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
sessionReady: make(chan struct{}),
|
||||
incomingMessages: make(chan *model.DirectionalMessage, msgBufferSize),
|
||||
}
|
||||
|
||||
// Create MUC client and set up message handling
|
||||
mucClient := &muc.Client{}
|
||||
client.mucClient = mucClient
|
||||
|
||||
// Create mux with MUC client and our message handler
|
||||
mux := mux.New("jabber:client",
|
||||
muc.HandleClient(mucClient),
|
||||
mux.MessageFunc(stanza.GroupChatMessage, xml.Name{}, client.handleIncomingMessage))
|
||||
client.mux = mux
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// NewClientWithTLS creates a new XMPP client with custom TLS configuration.
|
||||
|
@ -599,3 +616,110 @@ func (c *Client) Ping() error {
|
|||
c.logger.LogDebug("XMPP ping successful", "duration", duration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMessageChannel returns the channel for incoming messages (Bridge interface)
|
||||
func (c *Client) GetMessageChannel() <-chan *model.DirectionalMessage {
|
||||
return c.incomingMessages
|
||||
}
|
||||
|
||||
// handleIncomingMessage processes incoming XMPP message stanzas
|
||||
func (c *Client) handleIncomingMessage(msg stanza.Message, t xmlstream.TokenReadEncoder) error {
|
||||
c.logger.LogDebug("Received XMPP message",
|
||||
"from", msg.From.String(),
|
||||
"to", msg.To.String(),
|
||||
"type", fmt.Sprintf("%v", msg.Type))
|
||||
|
||||
// Only process groupchat messages for now (MUC messages from channels)
|
||||
if msg.Type != stanza.GroupChatMessage {
|
||||
c.logger.LogDebug("Ignoring non-groupchat message", "type", fmt.Sprintf("%v", msg.Type))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the message body from the token reader
|
||||
var msgWithBody struct {
|
||||
stanza.Message
|
||||
Body string `xml:"body"`
|
||||
}
|
||||
msgWithBody.Message = msg
|
||||
|
||||
d := xml.NewTokenDecoder(t)
|
||||
if err := d.DecodeElement(&msgWithBody, nil); err != nil {
|
||||
c.logger.LogError("Failed to decode message body", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if msgWithBody.Body == "" {
|
||||
c.logger.LogDebug("Message has no body, ignoring")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract channel and user information from JIDs
|
||||
channelID, err := c.extractChannelID(msg.From)
|
||||
if err != nil {
|
||||
c.logger.LogError("Failed to extract channel ID from JID", "from", msg.From.String(), "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
userID, userName := c.extractUserInfo(msg.From)
|
||||
|
||||
// Create BridgeMessage
|
||||
bridgeMsg := &model.BridgeMessage{
|
||||
SourceBridge: "xmpp",
|
||||
SourceChannelID: channelID,
|
||||
SourceUserID: userID,
|
||||
SourceUserName: userName,
|
||||
Content: msgWithBody.Body, // Already Markdown compatible
|
||||
MessageType: "text",
|
||||
Timestamp: time.Now(), // XMPP doesn't always provide timestamps
|
||||
MessageID: msg.ID,
|
||||
TargetBridges: []string{}, // Will be routed to all other bridges
|
||||
Metadata: map[string]any{
|
||||
"xmpp_from": msg.From.String(),
|
||||
"xmpp_to": msg.To.String(),
|
||||
},
|
||||
}
|
||||
|
||||
// Wrap in directional message
|
||||
directionalMsg := &model.DirectionalMessage{
|
||||
BridgeMessage: bridgeMsg,
|
||||
Direction: model.DirectionIncoming,
|
||||
}
|
||||
|
||||
// Send to message channel (non-blocking)
|
||||
select {
|
||||
case c.incomingMessages <- directionalMsg:
|
||||
c.logger.LogDebug("Message queued for processing",
|
||||
"channel_id", channelID,
|
||||
"user_id", userID,
|
||||
"content_length", len(msgWithBody.Body))
|
||||
default:
|
||||
c.logger.LogWarn("Message channel full, dropping message",
|
||||
"channel_id", channelID,
|
||||
"user_id", userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractChannelID extracts the channel ID (room bare JID) from a message JID
|
||||
func (c *Client) extractChannelID(from jid.JID) (string, error) {
|
||||
// For MUC messages, the channel ID is the bare JID (without resource/nickname)
|
||||
return from.Bare().String(), nil
|
||||
}
|
||||
|
||||
// extractUserInfo extracts user ID and display name from a message JID
|
||||
func (c *Client) extractUserInfo(from jid.JID) (string, string) {
|
||||
// For MUC messages, the resource part is the nickname
|
||||
nickname := from.Resourcepart()
|
||||
|
||||
// Use the full JID as user ID for XMPP
|
||||
userID := from.String()
|
||||
|
||||
// Use nickname as display name if available, otherwise use full JID
|
||||
displayName := nickname
|
||||
if displayName == "" {
|
||||
displayName = from.String()
|
||||
}
|
||||
|
||||
return userID, displayName
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue