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:
Felipe M 2025-08-04 11:29:35 +02:00
parent 1f45197aa8
commit a95ca8fb76
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
8 changed files with 454 additions and 17 deletions

View file

@ -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...)
}

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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),
}
}
}

View file

@ -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
}

View file

@ -153,6 +153,7 @@ func (p *Plugin) initXMPPClient() {
cfg.XMPPPassword,
cfg.GetXMPPResource(),
p.remoteID,
p.logger,
)
}

View file

@ -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
}