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

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