feat: implement comprehensive room validation and admin-only command access
- Add RoomExists and GetRoomMapping methods to Bridge interface - Implement XMPP room existence checking using disco#info queries (XEP-0030) - Add room validation in BridgeManager to prevent duplicate mappings and invalid rooms - Enhance XMPP client with CheckRoomExists method and comprehensive logging - Implement admin-only access control for all bridge commands - Add user-friendly error messages with actionable troubleshooting steps - Update doctor command with room existence testing and pre-join validation - Add SimpleLogger implementation for standalone command usage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1f45197aa8
commit
a95ca8fb76
8 changed files with 454 additions and 17 deletions
|
@ -28,6 +28,7 @@ type Config struct {
|
|||
TestRoom string
|
||||
TestMUC bool
|
||||
TestDirectMessage bool
|
||||
TestRoomExists bool
|
||||
Verbose bool
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
@ -43,14 +44,15 @@ func main() {
|
|||
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.TestDirectMessage, "test-dm", true, "Enable direct message testing (send message to admin user)")
|
||||
flag.BoolVar(&config.TestRoomExists, "test-room-exists", true, "Enable room existence testing using disco#info")
|
||||
flag.BoolVar(&config.Verbose, "verbose", true, "Enable verbose logging")
|
||||
flag.BoolVar(&config.InsecureSkipVerify, "insecure-skip-verify", true, "Skip TLS certificate verification (for development)")
|
||||
|
||||
flag.Usage = func() {
|
||||
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, "performing connection tests, optionally testing MUC room operations and direct messages,\n")
|
||||
fmt.Fprintf(os.Stderr, "and then disconnecting gracefully.\n\n")
|
||||
fmt.Fprintf(os.Stderr, "performing connection tests, room existence checks, optionally testing MUC room operations\n")
|
||||
fmt.Fprintf(os.Stderr, "and direct messages, and then disconnecting gracefully.\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage:\n")
|
||||
fmt.Fprintf(os.Stderr, " %s [flags]\n\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Examples:\n")
|
||||
|
@ -81,6 +83,9 @@ func main() {
|
|||
if config.TestDirectMessage {
|
||||
log.Printf(" Test Direct Messages: enabled")
|
||||
}
|
||||
if config.TestRoomExists {
|
||||
log.Printf(" Test Room Existence: enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Test the XMPP client
|
||||
|
@ -98,6 +103,9 @@ func main() {
|
|||
if config.TestDirectMessage {
|
||||
fmt.Println("✅ XMPP direct message test passed!")
|
||||
}
|
||||
if config.TestRoomExists {
|
||||
fmt.Println("✅ XMPP room existence test passed!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,6 +114,9 @@ func testXMPPClient(config *Config) error {
|
|||
log.Printf("Creating XMPP client...")
|
||||
}
|
||||
|
||||
// Create a simple logger for the XMPP client
|
||||
doctorLogger := &SimpleLogger{verbose: config.Verbose}
|
||||
|
||||
// Create XMPP client with optional TLS configuration
|
||||
var client *xmpp.Client
|
||||
if config.InsecureSkipVerify {
|
||||
|
@ -122,6 +133,7 @@ func testXMPPClient(config *Config) error {
|
|||
config.Resource,
|
||||
"doctor-remote-id",
|
||||
tlsConfig,
|
||||
doctorLogger,
|
||||
)
|
||||
} else {
|
||||
client = xmpp.NewClient(
|
||||
|
@ -130,6 +142,7 @@ func testXMPPClient(config *Config) error {
|
|||
config.Password,
|
||||
config.Resource,
|
||||
"doctor-remote-id",
|
||||
doctorLogger,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -164,6 +177,7 @@ func testXMPPClient(config *Config) error {
|
|||
|
||||
var mucDuration time.Duration
|
||||
var dmDuration time.Duration
|
||||
var roomExistsDuration time.Duration
|
||||
|
||||
// Test MUC operations if requested
|
||||
if config.TestMUC {
|
||||
|
@ -185,6 +199,16 @@ func testXMPPClient(config *Config) error {
|
|||
dmDuration = time.Since(start)
|
||||
}
|
||||
|
||||
// Test room existence if requested
|
||||
if config.TestRoomExists {
|
||||
start = time.Now()
|
||||
err = testRoomExists(client, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("room existence test failed: %w", err)
|
||||
}
|
||||
roomExistsDuration = time.Since(start)
|
||||
}
|
||||
|
||||
if config.Verbose {
|
||||
log.Printf("Disconnecting from XMPP server...")
|
||||
}
|
||||
|
@ -208,6 +232,9 @@ func testXMPPClient(config *Config) error {
|
|||
if config.TestDirectMessage {
|
||||
log.Printf(" Direct message time: %v", dmDuration)
|
||||
}
|
||||
if config.TestRoomExists {
|
||||
log.Printf(" Room existence check time: %v", roomExistsDuration)
|
||||
}
|
||||
log.Printf(" Disconnect time: %v", disconnectDuration)
|
||||
totalTime := connectDuration + pingDuration + disconnectDuration
|
||||
if config.TestMUC {
|
||||
|
@ -216,6 +243,9 @@ func testXMPPClient(config *Config) error {
|
|||
if config.TestDirectMessage {
|
||||
totalTime += dmDuration
|
||||
}
|
||||
if config.TestRoomExists {
|
||||
totalTime += roomExistsDuration
|
||||
}
|
||||
log.Printf(" Total time: %v", totalTime)
|
||||
}
|
||||
|
||||
|
@ -225,12 +255,33 @@ func testXMPPClient(config *Config) error {
|
|||
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...")
|
||||
log.Printf("First checking if room exists...")
|
||||
}
|
||||
|
||||
// Check if room exists before attempting to join
|
||||
start := time.Now()
|
||||
exists, err := client.CheckRoomExists(config.TestRoom)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check room existence for %s: %w", config.TestRoom, err)
|
||||
}
|
||||
checkDuration := time.Since(start)
|
||||
|
||||
if config.Verbose {
|
||||
log.Printf("✅ Room existence check completed in %v", checkDuration)
|
||||
log.Printf("Room %s exists: %t", config.TestRoom, exists)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("cannot test MUC operations: room %s does not exist or is not accessible", config.TestRoom)
|
||||
}
|
||||
|
||||
if config.Verbose {
|
||||
log.Printf("Room exists, proceeding to join...")
|
||||
}
|
||||
|
||||
// Test joining the room
|
||||
start := time.Now()
|
||||
err := client.JoinRoom(config.TestRoom)
|
||||
start = time.Now()
|
||||
err = client.JoinRoom(config.TestRoom)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to join MUC room %s: %w", config.TestRoom, err)
|
||||
}
|
||||
|
@ -281,11 +332,12 @@ func testMUCOperations(client *xmpp.Client, config *Config) error {
|
|||
if config.Verbose {
|
||||
log.Printf("✅ Successfully left MUC room in %v", leaveDuration)
|
||||
log.Printf("MUC operations summary:")
|
||||
log.Printf(" Room existence check time: %v", checkDuration)
|
||||
log.Printf(" Join time: %v", joinDuration)
|
||||
log.Printf(" Send message time: %v", sendDuration)
|
||||
log.Printf(" Wait time: 5s")
|
||||
log.Printf(" Leave time: %v", leaveDuration)
|
||||
log.Printf(" Total MUC time: %v", joinDuration+sendDuration+5*time.Second+leaveDuration)
|
||||
log.Printf(" Total MUC time: %v", checkDuration+joinDuration+sendDuration+5*time.Second+leaveDuration)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -319,9 +371,80 @@ func testDirectMessage(client *xmpp.Client, config *Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func testRoomExists(client *xmpp.Client, config *Config) error {
|
||||
if config.Verbose {
|
||||
log.Printf("Testing room existence functionality...")
|
||||
log.Printf("Checking if test room exists: %s", config.TestRoom)
|
||||
}
|
||||
|
||||
// Test room existence check
|
||||
start := time.Now()
|
||||
exists, err := client.CheckRoomExists(config.TestRoom)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check room existence for %s: %w", config.TestRoom, err)
|
||||
}
|
||||
checkDuration := time.Since(start)
|
||||
|
||||
if config.Verbose {
|
||||
log.Printf("✅ Room existence check completed in %v", checkDuration)
|
||||
log.Printf("Room %s exists: %t", config.TestRoom, exists)
|
||||
}
|
||||
|
||||
// Test with a non-existent room to verify negative case
|
||||
nonExistentRoom := "nonexistent-room-12345@conference.localhost"
|
||||
if config.Verbose {
|
||||
log.Printf("Testing negative case with non-existent room: %s", nonExistentRoom)
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
existsNegative, err := client.CheckRoomExists(nonExistentRoom)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check non-existent room %s: %w", nonExistentRoom, err)
|
||||
}
|
||||
checkNegativeDuration := time.Since(start)
|
||||
|
||||
if config.Verbose {
|
||||
log.Printf("✅ Negative room existence check completed in %v", checkNegativeDuration)
|
||||
log.Printf("Non-existent room %s exists: %t (should be false)", nonExistentRoom, existsNegative)
|
||||
log.Printf("Room existence test summary:")
|
||||
log.Printf(" Test room check time: %v", checkDuration)
|
||||
log.Printf(" Negative case check time: %v", checkNegativeDuration)
|
||||
log.Printf(" Total room existence test time: %v", checkDuration+checkNegativeDuration)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func maskPassword(password string) string {
|
||||
if len(password) <= 2 {
|
||||
return "****"
|
||||
}
|
||||
return password[:2] + "****"
|
||||
}
|
||||
|
||||
// SimpleLogger provides basic logging functionality for the doctor command
|
||||
type SimpleLogger struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// LogDebug logs debug messages if verbose mode is enabled
|
||||
func (l *SimpleLogger) LogDebug(msg string, args ...interface{}) {
|
||||
if l.verbose {
|
||||
log.Printf("[DEBUG] "+msg, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogInfo logs info messages
|
||||
func (l *SimpleLogger) LogInfo(msg string, args ...interface{}) {
|
||||
log.Printf("[INFO] "+msg, args...)
|
||||
}
|
||||
|
||||
// LogWarn logs warning messages
|
||||
func (l *SimpleLogger) LogWarn(msg string, args ...interface{}) {
|
||||
log.Printf("[WARN] "+msg, args...)
|
||||
}
|
||||
|
||||
// LogError logs error messages
|
||||
func (l *SimpleLogger) LogError(msg string, args ...interface{}) {
|
||||
log.Printf("[ERROR] "+msg, args...)
|
||||
}
|
|
@ -245,6 +245,39 @@ func (m *Manager) OnChannelMappingCreated(req model.ChannelMappingRequest) error
|
|||
return fmt.Errorf("bridge '%s' is not connected", req.BridgeName)
|
||||
}
|
||||
|
||||
// NEW: Check if room already mapped to another channel
|
||||
existingChannelID, err := bridge.GetRoomMapping(req.BridgeRoomID)
|
||||
if err != nil {
|
||||
m.logger.LogError("Failed to check room mapping", "bridge_room_id", req.BridgeRoomID, "error", err)
|
||||
return fmt.Errorf("failed to check room mapping: %w", err)
|
||||
}
|
||||
if existingChannelID != "" {
|
||||
m.logger.LogWarn("Room already mapped to another channel",
|
||||
"bridge_room_id", req.BridgeRoomID,
|
||||
"existing_channel_id", existingChannelID,
|
||||
"requested_channel_id", req.ChannelID)
|
||||
return fmt.Errorf("room '%s' is already mapped to channel '%s'", req.BridgeRoomID, existingChannelID)
|
||||
}
|
||||
|
||||
// NEW: Check if room exists on target bridge
|
||||
roomExists, err := bridge.RoomExists(req.BridgeRoomID)
|
||||
if err != nil {
|
||||
m.logger.LogError("Failed to check room existence", "bridge_room_id", req.BridgeRoomID, "error", err)
|
||||
return fmt.Errorf("failed to check room existence: %w", err)
|
||||
}
|
||||
if !roomExists {
|
||||
m.logger.LogWarn("Room does not exist on bridge",
|
||||
"bridge_room_id", req.BridgeRoomID,
|
||||
"bridge_name", req.BridgeName)
|
||||
return fmt.Errorf("room '%s' does not exist on %s bridge", req.BridgeRoomID, req.BridgeName)
|
||||
}
|
||||
|
||||
m.logger.LogDebug("Room validation passed",
|
||||
"bridge_room_id", req.BridgeRoomID,
|
||||
"bridge_name", req.BridgeName,
|
||||
"room_exists", roomExists,
|
||||
"already_mapped", false)
|
||||
|
||||
// Create the channel mapping on the receiving bridge
|
||||
if err = bridge.CreateChannelMapping(req.ChannelID, req.BridgeRoomID); err != nil {
|
||||
m.logger.LogError("Failed to create channel mapping", "channel_id", req.ChannelID, "bridge_name", req.BridgeName, "bridge_room_id", req.BridgeRoomID, "error", err)
|
||||
|
|
|
@ -255,3 +255,53 @@ func (b *mattermostBridge) DeleteChannelMapping(channelID string) error {
|
|||
b.logger.LogInfo("Deleted Mattermost channel room mapping", "channel_id", channelID, "room_id", roomID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoomExists checks if a Mattermost channel exists on the server
|
||||
func (b *mattermostBridge) RoomExists(roomID string) (bool, error) {
|
||||
if b.api == nil {
|
||||
return false, fmt.Errorf("Mattermost API not initialized")
|
||||
}
|
||||
|
||||
b.logger.LogDebug("Checking if Mattermost channel exists", "channel_id", roomID)
|
||||
|
||||
// Use the Mattermost API to check if the channel exists
|
||||
channel, appErr := b.api.GetChannel(roomID)
|
||||
if appErr != nil {
|
||||
if appErr.StatusCode == 404 {
|
||||
b.logger.LogDebug("Mattermost channel does not exist", "channel_id", roomID)
|
||||
return false, nil
|
||||
}
|
||||
b.logger.LogError("Failed to check channel existence", "channel_id", roomID, "error", appErr)
|
||||
return false, fmt.Errorf("failed to check channel existence: %w", appErr)
|
||||
}
|
||||
|
||||
if channel == nil {
|
||||
b.logger.LogDebug("Mattermost channel does not exist (nil response)", "channel_id", roomID)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
b.logger.LogDebug("Mattermost channel exists", "channel_id", roomID, "channel_name", channel.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetRoomMapping retrieves the Mattermost channel ID for a given room ID (reverse lookup)
|
||||
func (b *mattermostBridge) GetRoomMapping(roomID string) (string, error) {
|
||||
if b.kvstore == nil {
|
||||
return "", fmt.Errorf("KV store not initialized")
|
||||
}
|
||||
|
||||
b.logger.LogDebug("Getting channel mapping for Mattermost room", "room_id", roomID)
|
||||
|
||||
// Look up the channel ID using the room ID as the key
|
||||
channelIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("mattermost", roomID))
|
||||
if err != nil {
|
||||
// No mapping found is not an error, just return empty string
|
||||
b.logger.LogDebug("No channel mapping found for room", "room_id", roomID)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
channelID := string(channelIDBytes)
|
||||
b.logger.LogDebug("Found channel mapping for room", "room_id", roomID, "channel_id", channelID)
|
||||
|
||||
return channelID, nil
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ func (b *xmppBridge) createXMPPClient(cfg *config.Configuration) *xmppClient.Cli
|
|||
cfg.GetXMPPResource(),
|
||||
"", // remoteID not needed for bridge user
|
||||
tlsConfig,
|
||||
b.logger,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -471,3 +472,48 @@ func (b *xmppBridge) DeleteChannelMapping(channelID string) error {
|
|||
b.logger.LogInfo("Deleted channel room mapping", "channel_id", channelID, "room_jid", roomJID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoomExists checks if an XMPP room exists on the remote service
|
||||
func (b *xmppBridge) RoomExists(roomID string) (bool, error) {
|
||||
if !b.connected.Load() {
|
||||
return false, fmt.Errorf("not connected to XMPP server")
|
||||
}
|
||||
|
||||
if b.xmppClient == nil {
|
||||
return false, fmt.Errorf("XMPP client not initialized")
|
||||
}
|
||||
|
||||
b.logger.LogDebug("Checking if XMPP room exists", "room_jid", roomID)
|
||||
|
||||
// Use the XMPP client to check room existence
|
||||
exists, err := b.xmppClient.CheckRoomExists(roomID)
|
||||
if err != nil {
|
||||
b.logger.LogError("Failed to check room existence", "room_jid", roomID, "error", err)
|
||||
return false, fmt.Errorf("failed to check room existence: %w", err)
|
||||
}
|
||||
|
||||
b.logger.LogDebug("Room existence check completed", "room_jid", roomID, "exists", exists)
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// GetRoomMapping retrieves the Mattermost channel ID for a given XMPP room JID (reverse lookup)
|
||||
func (b *xmppBridge) GetRoomMapping(roomID string) (string, error) {
|
||||
if b.kvstore == nil {
|
||||
return "", fmt.Errorf("KV store not initialized")
|
||||
}
|
||||
|
||||
b.logger.LogDebug("Getting channel mapping for XMPP room", "room_jid", roomID)
|
||||
|
||||
// Look up the channel ID using the room JID as the key
|
||||
channelIDBytes, err := b.kvstore.Get(kvstore.BuildChannelMapKey("xmpp", roomID))
|
||||
if err != nil {
|
||||
// No mapping found is not an error, just return empty string
|
||||
b.logger.LogDebug("No channel mapping found for room", "room_jid", roomID)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
channelID := string(channelIDBytes)
|
||||
b.logger.LogDebug("Found channel mapping for room", "room_jid", roomID, "channel_id", channelID)
|
||||
|
||||
return channelID, nil
|
||||
}
|
||||
|
|
|
@ -54,6 +54,14 @@ func NewCommandHandler(client *pluginapi.Client, bridgeManager pluginModel.Bridg
|
|||
|
||||
// ExecuteCommand hook calls this method to execute the commands that were registered in the NewCommandHandler function.
|
||||
func (c *Handler) Handle(args *model.CommandArgs) (*model.CommandResponse, error) {
|
||||
// Check if user is system admin for all plugin commands
|
||||
if !c.isSystemAdmin(args.UserId) {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: "❌ Only system administrators can use XMPP bridge commands.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
trigger := strings.TrimPrefix(strings.Fields(args.Command)[0], "/")
|
||||
switch trigger {
|
||||
case xmppBridgeCommandTrigger:
|
||||
|
@ -162,10 +170,7 @@ func (c *Handler) executeMapCommand(args *model.CommandArgs, fields []string) *m
|
|||
|
||||
err = c.bridgeManager.OnChannelMappingCreated(mappingReq)
|
||||
if err != nil {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf("❌ Failed to create channel mapping: %v", err),
|
||||
}
|
||||
return c.formatMappingError("create", roomJID, err)
|
||||
}
|
||||
|
||||
return &model.CommandResponse{
|
||||
|
@ -212,10 +217,7 @@ func (c *Handler) executeUnmapCommand(args *model.CommandArgs) *model.CommandRes
|
|||
|
||||
err = c.bridgeManager.OnChannelMappingDeleted(deleteReq)
|
||||
if err != nil {
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf("❌ Failed to unmap channel: %v", err),
|
||||
}
|
||||
return c.formatMappingError("delete", roomJID, err)
|
||||
}
|
||||
|
||||
return &model.CommandResponse{
|
||||
|
@ -269,3 +271,85 @@ func (c *Handler) executeStatusCommand(args *model.CommandArgs) *model.CommandRe
|
|||
- Use `+"`/xmppbridge unmap`"+` to unmap this channel from an XMPP room`, statusText, mappingText),
|
||||
}
|
||||
}
|
||||
|
||||
// isSystemAdmin checks if the user is a system administrator
|
||||
func (c *Handler) isSystemAdmin(userID string) bool {
|
||||
user, err := c.client.User.Get(userID)
|
||||
if err != nil {
|
||||
c.client.Log.Warn("Failed to get user for admin check", "user_id", userID, "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return user.IsSystemAdmin()
|
||||
}
|
||||
|
||||
// formatMappingError provides user-friendly error messages for mapping operations
|
||||
func (c *Handler) formatMappingError(operation, roomJID string, err error) *model.CommandResponse {
|
||||
errorMsg := err.Error()
|
||||
|
||||
// Handle specific error cases with user-friendly messages
|
||||
switch {
|
||||
case strings.Contains(errorMsg, "already mapped to channel"):
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf(`❌ **Room Already Mapped**
|
||||
|
||||
The XMPP room **%s** is already connected to another channel.
|
||||
|
||||
**What you can do:**
|
||||
- Choose a different XMPP room that isn't already in use
|
||||
- Unmap the room from the other channel first using ` + "`/xmppbridge unmap`" + `
|
||||
- Use ` + "`/xmppbridge status`" + ` to check current mappings`, roomJID),
|
||||
}
|
||||
|
||||
case strings.Contains(errorMsg, "does not exist"):
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf(`❌ **Room Not Found**
|
||||
|
||||
The XMPP room **%s** doesn't exist or isn't accessible.
|
||||
|
||||
**What you can do:**
|
||||
- Check that the room JID is spelled correctly
|
||||
- Make sure the room exists on the XMPP server
|
||||
- Verify you have permission to access the room
|
||||
- Contact your XMPP administrator if needed
|
||||
|
||||
**Example format:** room@conference.example.com`, roomJID),
|
||||
}
|
||||
|
||||
case strings.Contains(errorMsg, "not connected"):
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: `❌ **Bridge Not Connected**
|
||||
|
||||
The XMPP bridge is currently disconnected.
|
||||
|
||||
**What you can do:**
|
||||
- Wait a moment and try again (the bridge may be reconnecting)
|
||||
- Contact your system administrator
|
||||
- Use ` + "`/xmppbridge status`" + ` to check the connection status`,
|
||||
}
|
||||
|
||||
default:
|
||||
// Generic error message for unknown cases
|
||||
action := "create the mapping"
|
||||
if operation == "delete" {
|
||||
action = "remove the mapping"
|
||||
}
|
||||
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.CommandResponseTypeEphemeral,
|
||||
Text: fmt.Sprintf(`❌ **Operation Failed**
|
||||
|
||||
Unable to %s for room **%s**.
|
||||
|
||||
**What you can do:**
|
||||
- Try the command again in a few moments
|
||||
- Use ` + "`/xmppbridge status`" + ` to check the bridge status
|
||||
- Contact your system administrator if the problem persists
|
||||
|
||||
**Error details:** %s`, action, roomJID, errorMsg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,12 @@ type Bridge interface {
|
|||
// DeleteChannelMapping removes a mapping between a Mattermost channel ID and a bridge room ID.
|
||||
DeleteChannelMapping(channelID string) error
|
||||
|
||||
// RoomExists checks if a room/channel exists on the remote service.
|
||||
RoomExists(roomID string) (bool, error)
|
||||
|
||||
// GetRoomMapping retrieves the Mattermost channel ID for a given room ID (reverse lookup).
|
||||
GetRoomMapping(roomID string) (string, error)
|
||||
|
||||
// IsConnected checks if the bridge is connected to the remote service.
|
||||
IsConnected() bool
|
||||
}
|
||||
|
|
|
@ -153,6 +153,7 @@ func (p *Plugin) initXMPPClient() {
|
|||
cfg.XMPPPassword,
|
||||
cfg.GetXMPPResource(),
|
||||
p.remoteID,
|
||||
p.logger,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,10 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
||||
"mellium.im/sasl"
|
||||
"mellium.im/xmpp"
|
||||
"mellium.im/xmpp/disco"
|
||||
"mellium.im/xmpp/jid"
|
||||
"mellium.im/xmpp/muc"
|
||||
"mellium.im/xmpp/mux"
|
||||
|
@ -25,6 +27,7 @@ type Client struct {
|
|||
remoteID string // Plugin remote ID for metadata
|
||||
serverDomain string // explicit server domain for testing
|
||||
tlsConfig *tls.Config // custom TLS configuration
|
||||
logger logger.Logger // Logger for debugging
|
||||
|
||||
// XMPP connection
|
||||
session *xmpp.Session
|
||||
|
@ -80,7 +83,7 @@ type UserProfile struct {
|
|||
}
|
||||
|
||||
// NewClient creates a new XMPP client.
|
||||
func NewClient(serverURL, username, password, resource, remoteID string) *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))
|
||||
|
@ -91,6 +94,7 @@ func NewClient(serverURL, username, password, resource, remoteID string) *Client
|
|||
password: password,
|
||||
resource: resource,
|
||||
remoteID: remoteID,
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
mucClient: mucClient,
|
||||
|
@ -100,8 +104,8 @@ func NewClient(serverURL, username, password, resource, remoteID string) *Client
|
|||
}
|
||||
|
||||
// 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)
|
||||
func NewClientWithTLS(serverURL, username, password, resource, remoteID string, tlsConfig *tls.Config, logger logger.Logger) *Client {
|
||||
client := NewClient(serverURL, username, password, resource, remoteID, logger)
|
||||
client.tlsConfig = tlsConfig
|
||||
return client
|
||||
}
|
||||
|
@ -430,3 +434,93 @@ func (c *Client) SetOnlinePresence() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckRoomExists verifies if an XMPP room exists and is accessible using disco#info
|
||||
func (c *Client) CheckRoomExists(roomJID string) (bool, error) {
|
||||
if c.session == nil {
|
||||
return false, fmt.Errorf("XMPP session not established")
|
||||
}
|
||||
|
||||
c.logger.LogDebug("Checking room existence using disco#info", "room_jid", roomJID)
|
||||
|
||||
// Parse and validate the room JID
|
||||
roomAddr, err := jid.Parse(roomJID)
|
||||
if err != nil {
|
||||
c.logger.LogError("Invalid room JID", "room_jid", roomJID, "error", err)
|
||||
return false, fmt.Errorf("invalid room JID: %w", err)
|
||||
}
|
||||
|
||||
// Set timeout for the disco query
|
||||
ctx, cancel := context.WithTimeout(c.ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Perform disco#info query to the room
|
||||
info, err := disco.GetInfo(ctx, "", roomAddr, c.session)
|
||||
if err != nil {
|
||||
// Check if it's a service-unavailable or item-not-found error
|
||||
if stanzaErr, ok := err.(stanza.Error); ok {
|
||||
c.logger.LogDebug("Received stanza error during disco#info query",
|
||||
"room_jid", roomJID,
|
||||
"error_condition", string(stanzaErr.Condition),
|
||||
"error_type", string(stanzaErr.Type))
|
||||
|
||||
switch stanzaErr.Condition {
|
||||
case stanza.ServiceUnavailable, stanza.ItemNotFound:
|
||||
c.logger.LogDebug("Room does not exist", "room_jid", roomJID, "condition", string(stanzaErr.Condition))
|
||||
return false, nil // Room doesn't exist
|
||||
case stanza.Forbidden:
|
||||
c.logger.LogWarn("Access denied to room (room exists but not accessible)", "room_jid", roomJID)
|
||||
return false, fmt.Errorf("access denied to room %s", roomJID)
|
||||
case stanza.NotAuthorized:
|
||||
c.logger.LogWarn("Not authorized to query room (room exists but not queryable)", "room_jid", roomJID)
|
||||
return false, fmt.Errorf("not authorized to query room %s", roomJID)
|
||||
default:
|
||||
c.logger.LogError("Unexpected disco query error", "room_jid", roomJID, "condition", string(stanzaErr.Condition), "error", err)
|
||||
return false, fmt.Errorf("disco query failed: %w", err)
|
||||
}
|
||||
}
|
||||
c.logger.LogError("Disco query error", "room_jid", roomJID, "error", err)
|
||||
return false, fmt.Errorf("disco query error: %w", err)
|
||||
}
|
||||
|
||||
c.logger.LogDebug("Received disco#info response, checking for MUC features",
|
||||
"room_jid", roomJID,
|
||||
"features_count", len(info.Features),
|
||||
"identities_count", len(info.Identity))
|
||||
|
||||
// Verify it's actually a MUC room by checking features
|
||||
for _, feature := range info.Features {
|
||||
if feature.Var == muc.NS { // "http://jabber.org/protocol/muc"
|
||||
c.logger.LogDebug("Room exists and has MUC feature", "room_jid", roomJID)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check for conference identity as backup verification
|
||||
for _, identity := range info.Identity {
|
||||
if identity.Category == "conference" {
|
||||
c.logger.LogDebug("Room exists and has conference identity", "room_jid", roomJID, "identity_type", identity.Type)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Log all features and identities for debugging
|
||||
c.logger.LogDebug("Room exists but doesn't appear to be a MUC room",
|
||||
"room_jid", roomJID,
|
||||
"features", func() []string {
|
||||
var features []string
|
||||
for _, f := range info.Features {
|
||||
features = append(features, f.Var)
|
||||
}
|
||||
return features
|
||||
}(),
|
||||
"identities", func() []string {
|
||||
var identities []string
|
||||
for _, i := range info.Identity {
|
||||
identities = append(identities, fmt.Sprintf("%s/%s", i.Category, i.Type))
|
||||
}
|
||||
return identities
|
||||
}())
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue