diff --git a/bot/button/handlers/addadmin.go b/bot/button/handlers/addadmin.go index 1d283ee4..138430f5 100644 --- a/bot/button/handlers/addadmin.go +++ b/bot/button/handlers/addadmin.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/TicketsBot-cloud/common/botpermissions" permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/channel" "github.com/TicketsBot-cloud/gdl/permission" @@ -19,7 +20,6 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command/context" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" - "github.com/TicketsBot-cloud/worker/bot/logic" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -224,7 +224,7 @@ func (h *AddAdminHandler) Execute(ctx *context.ButtonContext) { overwrites := append(ch.PermissionOverwrites, channel.PermissionOverwrite{ Id: id, Type: mentionableType.OverwriteType(), - Allow: permission.BuildPermissions(logic.StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) diff --git a/bot/button/handlers/addsupport.go b/bot/button/handlers/addsupport.go index ab90e3de..c7162815 100644 --- a/bot/button/handlers/addsupport.go +++ b/bot/button/handlers/addsupport.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/TicketsBot-cloud/common/botpermissions" permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel" @@ -21,7 +22,6 @@ import ( cmdregistry "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" - "github.com/TicketsBot-cloud/worker/bot/logic" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -248,7 +248,7 @@ func updateChannelPermissions(ctx cmdregistry.CommandContext, id uint64, mention overwrites := append(ch.PermissionOverwrites, channel.PermissionOverwrite{ Id: id, Type: mentionableType.OverwriteType(), - Allow: permission.BuildPermissions(logic.StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) diff --git a/bot/button/handlers/admindebug/server/blacklistreason.go b/bot/button/handlers/admindebug/server/blacklistreason.go index f399a598..2b923786 100644 --- a/bot/button/handlers/admindebug/server/blacklistreason.go +++ b/bot/button/handlers/admindebug/server/blacklistreason.go @@ -127,7 +127,7 @@ func (h *AdminDebugServerBlacklistReasonHandler) Execute(ctx *context.ButtonCont } ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Red, "Admin - Debug Server - Blacklist Reason", diff --git a/bot/button/handlers/admindebug/server/entitlements.go b/bot/button/handlers/admindebug/server/entitlements.go index f4c8cab7..b17f1500 100644 --- a/bot/button/handlers/admindebug/server/entitlements.go +++ b/bot/button/handlers/admindebug/server/entitlements.go @@ -103,7 +103,7 @@ func (h *AdminDebugServerEntitlementsHandler) Execute(ctx *context.ButtonContext } ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Green, "Admin - Debug Server - Entitlements", diff --git a/bot/button/handlers/admindebug/server/modals/panelsettings.go b/bot/button/handlers/admindebug/server/modals/panelsettings.go index 94d00773..fb0d52e5 100644 --- a/bot/button/handlers/admindebug/server/modals/panelsettings.go +++ b/bot/button/handlers/admindebug/server/modals/panelsettings.go @@ -125,7 +125,7 @@ func (h *AdminDebugServerPanelSettingsModalHandler) Execute(ctx *context.ModalCo } ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Orange, "Admin - Debug Server - Panel Settings", diff --git a/bot/button/handlers/admindebug/server/modals/permissions.go b/bot/button/handlers/admindebug/server/modals/permissions.go index 1cb3305e..1c1ea967 100644 --- a/bot/button/handlers/admindebug/server/modals/permissions.go +++ b/bot/button/handlers/admindebug/server/modals/permissions.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/TicketsBot-cloud/common/botpermissions" permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/interaction" @@ -20,7 +21,6 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command/context" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" - "github.com/TicketsBot-cloud/worker/bot/logic" "github.com/TicketsBot-cloud/worker/bot/permissionwrapper" "github.com/TicketsBot-cloud/worker/bot/utils" ) @@ -100,46 +100,88 @@ func (h *AdminDebugServerPermissionsModalSubmitHandler) Execute(ctx *context.Mod return } - // Process permission checks using shared logic - results, hasMissingPermissions := processPermissionChecks(selectedValues, worker, guildId, botMember, panels) + sections := processPermissionChecks(selectedValues, worker, guildId, botMember, panels) - // Choose color based on whether permissions are missing - colour := customisation.Green - if hasMissingPermissions { - colour = customisation.Orange + // Ack first so every chunk is a follow-up; the initial reply is sent by another goroutine + ctx.Ack() + + var chunk []component.Component + var chunkText int + + flush := func() bool { + if len(chunk) == 0 { + return true + } + + if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(chunk)); err != nil { + ctx.HandleError(err) + return false + } + + chunk, chunkText = nil, 0 + return true + } + + for _, section := range sections { + cost := len(section.title) + len(section.body) + if len(chunk) >= maxContainersPerMessage || (len(chunk) > 0 && chunkText+cost > maxTextPerMessage) { + if !flush() { + return + } + } + + colour := customisation.Green + if section.hasMissing { + colour = customisation.Orange + } + + chunk = append(chunk, utils.BuildAdminContainerRaw(ctx, colour, section.title, section.body)) + chunkText += cost } - ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( - ctx, - colour, - "Admin - Debug Server - Permissions Check", - strings.Join(results, "\n\n"), - ), - })) + flush() } -func processPermissionChecks(selectedValues []string, worker *w.Context, guildId uint64, botMember member.Member, panels []database.Panel) ([]string, bool) { - // Server-wide permissions - serverWidePermissions := append( - []permission.Permission{ - // Thread mode specific - permission.CreatePrivateThreads, - permission.SendMessagesInThreads, - permission.ManageThreads, - // Channel mode specific - permission.ManageChannels, - // Both modes - permission.ManageWebhooks, - permission.PinMessages, - // Server-wide only - permission.ManageRoles, - }, - logic.StandardPermissions[:]..., - ) +const ( + maxComponentsPerMessage = 40 + componentsPerContainer = 4 + maxContainersPerMessage = maxComponentsPerMessage / componentsPerContainer + maxTextPerMessage = 3900 +) - var results []string - var hasMissingPermissions bool +var pendingCategoryPerms = []permission.Permission{permission.ViewChannel, permission.ManageChannels} + +type permissionSection struct { + title string + body string + hasMissing bool +} + +func processPermissionChecks(selectedValues []string, worker *w.Context, guildId uint64, botMember member.Member, panels []database.Panel) []permissionSection { + serverWidePermissions := []permission.Permission{ + permission.ManageWebhooks, + permission.PinMessages, + permission.ManageRoles, + permission.ManageChannels, + // /notes opens a private thread inside a channel-mode ticket + permission.CreatePrivateThreads, + permission.SendMessagesInThreads, + } + + anyThread := len(panels) == 0 + for _, p := range panels { + if p.UseThreads { + anyThread = true + break + } + } + if anyThread { + serverWidePermissions = append(serverWidePermissions, permission.ManageThreads) + } + + serverWidePermissions = append(serverWidePermissions, botpermissions.StandardPermissions...) + + var sections []permissionSection for _, value := range selectedValues { parts := strings.Split(value, "_") @@ -147,11 +189,12 @@ func processPermissionChecks(selectedValues []string, worker *w.Context, guildId switch checkType { case "server": - result, hasMissing := checkServerWidePermissions(worker, guildId, botMember, serverWidePermissions) - results = append(results, fmt.Sprintf("**Server Wide Permissions**\n%s", result)) - if hasMissing { - hasMissingPermissions = true - } + body, hasMissing := checkServerWidePermissions(worker, guildId, botMember, serverWidePermissions) + sections = append(sections, permissionSection{ + title: "Server Wide Permissions", + body: body, + hasMissing: hasMissing, + }) case "panel": if len(parts) < 2 { @@ -162,7 +205,6 @@ func processPermissionChecks(selectedValues []string, worker *w.Context, guildId continue } - // Find the panel var panel *database.Panel for i := range panels { if panels[i].MessageId == panelMessageId { @@ -172,20 +214,24 @@ func processPermissionChecks(selectedValues []string, worker *w.Context, guildId } if panel == nil { - results = append(results, fmt.Sprintf("**Panel (ID: %d)**\nPanel not found", panelMessageId)) + sections = append(sections, permissionSection{ + title: fmt.Sprintf("Panel (ID: %d)", panelMessageId), + body: "Panel not found", + hasMissing: true, + }) continue } - // Check permissions for this panel - panelResults, hasMissing := checkPanelPermissions(worker, guildId, botMember, *panel) - results = append(results, fmt.Sprintf("**Panel: %s**\n%s", panel.Title, panelResults)) - if hasMissing { - hasMissingPermissions = true - } + body, hasMissing := checkPanelPermissions(worker, guildId, botMember, *panel) + sections = append(sections, permissionSection{ + title: fmt.Sprintf("Panel: %s", panel.Title), + body: body, + hasMissing: hasMissing, + }) } } - return results, hasMissingPermissions + return sections } func checkServerWidePermissions(worker *w.Context, guildId uint64, botMember member.Member, requiredPermissions []permission.Permission) (string, bool) { @@ -210,7 +256,7 @@ func checkServerWidePermissions(worker *w.Context, guildId uint64, botMember mem if len(missing) > 0 { result.WriteString("**Missing Permissions:**\n") for _, p := range missing { - result.WriteString(fmt.Sprintf("- %s\n", p)) + result.WriteString(fmt.Sprintf("- `%s`\n", p)) } } else { result.WriteString("All required permissions are present\n") @@ -230,20 +276,9 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M if panel.ChannelId != 0 { var panelChannelPerms []permission.Permission if usesThreads { - // Thread mode: specific thread permissions + standard permissions - panelChannelPerms = append( - []permission.Permission{ - permission.CreatePrivateThreads, - permission.SendMessagesInThreads, - permission.ManageThreads, - permission.ManageWebhooks, - permission.PinMessages, - }, - logic.StandardPermissions[:]..., - ) + panelChannelPerms = botpermissions.ThreadModeRequired } else { - // Channel mode: just standard permissions (no special ones needed for panel channel in channel mode) - panelChannelPerms = append([]permission.Permission{}, logic.StandardPermissions[:]...) + panelChannelPerms = botpermissions.StandardPermissions } result, hasMissing := checkChannelPermissions(worker, panel.ChannelId, botMember, guildId, panelChannelPerms, "Panel Channel") results = append(results, result) @@ -254,26 +289,33 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M // Check category permissions if using channel mode if !usesThreads && panel.TargetCategory != 0 { - // Category needs channel management permissions + standard permissions - categoryPerms := append( - []permission.Permission{ - permission.ManageChannels, - permission.ManageWebhooks, - permission.PinMessages, - }, - logic.StandardPermissions[:]..., - ) + categoryPerms := botpermissions.ChannelModeRequired result, hasMissing := checkChannelPermissions(worker, panel.TargetCategory, botMember, guildId, categoryPerms, "Category") results = append(results, result) if hasMissing { hasMissingPermissions = true } + + if panel.OverflowEnabled && panel.OverflowCategoryId != nil { + result, hasMissing := checkChannelPermissions(worker, *panel.OverflowCategoryId, botMember, guildId, categoryPerms, "Overflow Category") + results = append(results, result) + if hasMissing { + hasMissingPermissions = true + } + } + + if panel.PendingCategory != nil { + result, hasMissing := checkChannelPermissions(worker, *panel.PendingCategory, botMember, guildId, pendingCategoryPerms, "Pending Category") + results = append(results, result) + if hasMissing { + hasMissingPermissions = true + } + } } // Check transcript channel if enabled for this panel if panel.TranscriptChannelId != nil { - // Transcript channel needs minimal message permissions - transcriptPerms := append([]permission.Permission{}, logic.MinimalPermissions[:]...) + transcriptPerms := botpermissions.TranscriptChannelRequired result, hasMissing := checkChannelPermissions(worker, *panel.TranscriptChannelId, botMember, guildId, transcriptPerms, "Transcript Channel") results = append(results, result) if hasMissing { @@ -281,16 +323,8 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M } } - // Check notification channel if using thread mode - if usesThreads && panel.TicketNotificationChannel != nil { - notificationPerms := append( - []permission.Permission{ - permission.EmbedLinks, - permission.AttachFiles, - }, - logic.MinimalPermissions[:]..., - ) - result, hasMissing := checkChannelPermissions(worker, *panel.TicketNotificationChannel, botMember, guildId, notificationPerms, "Notification Channel") + if panel.TicketNotificationChannel != nil { + result, hasMissing := checkChannelPermissions(worker, *panel.TicketNotificationChannel, botMember, guildId, botpermissions.NotifChannelRequired, "Notification Channel") results = append(results, result) if hasMissing { hasMissingPermissions = true @@ -305,18 +339,15 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M } func checkChannelPermissions(worker *w.Context, channelId uint64, botMember member.Member, guildId uint64, requiredPermissions []permission.Permission, label string) (string, bool) { - channel, err := worker.GetChannel(channelId) - if err != nil && !utils.IsMissingAccess(err) { - return fmt.Sprintf("**%s**\nError: Could not fetch channel", label), false + missingPerms, channel, err := permissionwrapper.GetMissingPermissionsChannel(worker, guildId, botMember.User.Id, channelId, requiredPermissions...) + if errors.Is(err, permissionwrapper.ErrChannelObfuscated) { + return fmt.Sprintf("**%s** (`%d`)\n**Missing Permissions:**\n- `%s`\n", label, channelId, permission.ViewChannel.ChannelName(channel.Type)), true } - if err != nil || channel.IsObfuscated() { - return fmt.Sprintf("**%s** (`%d`)\n**Missing Permissions:**\n- View Channel\n", label, channelId), true + if err != nil { + return fmt.Sprintf("**%s**\nError: Could not fetch channel", label), false } - // Use permissionwrapper to get missing permissions - missingPerms := permissionwrapper.GetMissingPermissionsChannel(worker, guildId, botMember.User.Id, channelId, requiredPermissions...) - // Create a map for quick lookup of missing permissions missingMap := make(map[permission.Permission]bool) for _, perm := range missingPerms { @@ -327,7 +358,7 @@ func checkChannelPermissions(worker *w.Context, channelId uint64, botMember memb var missing []string for _, perm := range requiredPermissions { if missingMap[perm] { - missing = append(missing, perm.String()) + missing = append(missing, perm.ChannelName(channel.Type)) } } @@ -336,7 +367,7 @@ func checkChannelPermissions(worker *w.Context, channelId uint64, botMember memb if len(missing) > 0 { result.WriteString("**Missing Permissions:**\n") for _, p := range missing { - result.WriteString(fmt.Sprintf("- %s\n", p)) + result.WriteString(fmt.Sprintf("- `%s`\n", p)) } } else { result.WriteString("All required permissions are present\n") diff --git a/bot/button/handlers/admindebug/server/modals/ticketpermissions.go b/bot/button/handlers/admindebug/server/modals/ticketpermissions.go index fe9b5f77..28ca672f 100644 --- a/bot/button/handlers/admindebug/server/modals/ticketpermissions.go +++ b/bot/button/handlers/admindebug/server/modals/ticketpermissions.go @@ -171,7 +171,7 @@ func (h *AdminDebugServerTicketPermissionsModalSubmitHandler) Execute(ctx *conte } ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Green, "Admin - Debug Server - User Permissions Check", diff --git a/bot/button/handlers/admindebug/server/modals/usertickets.go b/bot/button/handlers/admindebug/server/modals/usertickets.go index 98b95302..35a9ea06 100644 --- a/bot/button/handlers/admindebug/server/modals/usertickets.go +++ b/bot/button/handlers/admindebug/server/modals/usertickets.go @@ -121,7 +121,7 @@ func (h *AdminDebugServerUserTicketsModalSubmitHandler) Execute(ctx *context.Mod } ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Green, "Admin - Debug Server - User Tickets", diff --git a/bot/button/handlers/admindebug/server/monitoredbots.go b/bot/button/handlers/admindebug/server/monitoredbots.go index 614acc22..6111ba12 100644 --- a/bot/button/handlers/admindebug/server/monitoredbots.go +++ b/bot/button/handlers/admindebug/server/monitoredbots.go @@ -72,7 +72,7 @@ func (h *AdminDebugServerMonitoredBotsHandler) Execute(ctx *context.ButtonContex } ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Orange, "Admin - Debug Server - Monitored Bots", diff --git a/bot/button/handlers/admindebug/server/recache.go b/bot/button/handlers/admindebug/server/recache.go index 96fc3cc3..01b93d55 100644 --- a/bot/button/handlers/admindebug/server/recache.go +++ b/bot/button/handlers/admindebug/server/recache.go @@ -43,7 +43,7 @@ func (h *AdminDebugServerRecacheHandler) Execute(ctx *context.ButtonContext) { if onCooldown, cooldownTime := redis.GetRecacheCooldown(guildId); onCooldown { ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents( + utils.BuildAdminContainerWithComponents( ctx, customisation.Red, "Admin - Recache", @@ -86,7 +86,7 @@ func (h *AdminDebugServerRecacheHandler) Execute(ctx *context.ButtonContext) { } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents( + utils.BuildAdminContainerWithComponents( ctx, customisation.Orange, "Admin - Recache", diff --git a/bot/command/context/replyable.go b/bot/command/context/replyable.go index 95b221e5..544bdaf7 100644 --- a/bot/command/context/replyable.go +++ b/bot/command/context/replyable.go @@ -10,10 +10,12 @@ import ( "strings" "time" + "github.com/TicketsBot-cloud/common/botpermissions" permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/database" + "github.com/TicketsBot-cloud/gdl/objects/channel" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" @@ -24,7 +26,6 @@ import ( "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/logging" - "github.com/TicketsBot-cloud/worker/bot/logic" "github.com/TicketsBot-cloud/worker/bot/permissionwrapper" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/config" @@ -250,15 +251,21 @@ func (r *Replyable) buildErrorResponse(err error, eventId string, includeInviteL interactionCtx, ok := r.ctx.(registry.InteractionContext) docsUrl := fmt.Sprintf("%s/miscellaneous/permissions-explained", config.Conf.Bot.DocsUrl) if ok { - missingPermissions, err := findMissingPermissions(interactionCtx) + missingByLocation, err := findMissingPermissions(interactionCtx) if err == nil { - if len(missingPermissions) > 0 { - message = r.GetMessage(i18n.MessageErrorMissingPermissionsTitle) + ":\n" - for _, perm := range missingPermissions { - message += fmt.Sprintf("* `%s`\n", perm.String()) + if len(missingByLocation) > 0 { + title := r.GetMessage(i18n.MessageErrorMissingPermissionsTitle) + ":\n" + footer := "\n" + r.GetMessage(i18n.MessageErrorMissingPermissionsBody, docsUrl) + + var locationMsg string + for _, loc := range missingByLocation { + locationMsg += fmt.Sprintf("**%s:**\n", loc.label) + for _, perm := range loc.missing { + locationMsg += fmt.Sprintf("* `%s`\n", loc.permissionName(perm)) + } } - message += "\n" + r.GetMessage(i18n.MessageErrorMissingPermissionsBody, docsUrl) + message = title + locationMsg + footer } else { message = r.GetMessage(i18n.MessageErrorMissingAccess, docsUrl) } @@ -346,61 +353,124 @@ func (r *Replyable) formatDiscordError(restError request.RestError, eventId stri r.GetMessage(i18n.MessageErrorId) + ": `" + eventId + "`" } -func findMissingPermissions(ctx registry.InteractionContext) ([]permission.Permission, error) { +type missingPermLocation struct { + label string + missing []permission.Permission + channelType *channel.ChannelType // nil when guild-level or the channel couldn't be fetched (ChannelTypeGuildText is 0) +} + +func (l missingPermLocation) permissionName(p permission.Permission) string { + if l.channelType == nil { + return p.String() + } + + return p.ChannelName(*l.channelType) +} + +func findMissingPermissions(ctx registry.InteractionContext) ([]missingPermLocation, error) { if permission.HasPermissionRaw(ctx.InteractionMetadata().AppPermissions, permission.Administrator) { return nil, nil } var useThreads bool - var targetChannelId uint64 + + var panelCustomId string + switch c := ctx.(type) { + case *ButtonContext: + panelCustomId = c.InteractionData.CustomId + case *SelectMenuContext: + if len(c.InteractionData.Values) > 0 { + panelCustomId = c.InteractionData.Values[0] + } + case *ModalContext: + panelCustomId = strings.TrimPrefix(c.Interaction.Data.CustomId, "form_") + } var panel *database.Panel - if btnCtx, ok := ctx.(*ButtonContext); ok { - p, panelExists, err := dbclient.Client.Panel.GetByCustomId(context.Background(), ctx.GuildId(), btnCtx.InteractionData.CustomId) + if panelCustomId != "" { + p, panelExists, err := dbclient.Client.Panel.GetByCustomId(context.Background(), ctx.GuildId(), panelCustomId) if err == nil && panelExists { panel = &p useThreads = panel.UseThreads } } + checkChannel := func(channelId uint64, label string, required []permission.Permission) missingPermLocation { + missing, ch, _ := permissionwrapper.GetMissingPermissionsChannel(ctx.Worker(), ctx.GuildId(), ctx.Worker().BotId, channelId, required...) + loc := missingPermLocation{label: label, missing: missing} + if ch.Id != 0 { + loc.channelType = &ch.Type + } + return loc + } + + var locations []missingPermLocation + + var primaryChannelId uint64 + var primaryLabel string + var primaryRequired []permission.Permission + if useThreads { - // Thread mode - check permissions in the current channel - targetChannelId = ctx.ChannelId() + primaryChannelId = ctx.ChannelId() + primaryLabel = "Panel channel" + primaryRequired = botpermissions.ThreadModeRequired } else { - // Channel mode - check permissions in the panel's category (0 = no category, skip check) + primaryLabel = "Ticket category" + primaryRequired = botpermissions.ChannelModeRequired if panel != nil { - targetChannelId = panel.TargetCategory + primaryChannelId = panel.TargetCategory } } - // Build required permissions based on mode - var requiredPermissions []permission.Permission - if useThreads { - // Thread mode permissions - requiredPermissions = append( - []permission.Permission{ - permission.CreatePrivateThreads, - permission.SendMessagesInThreads, - permission.ManageThreads, - }, - logic.StandardPermissions[:]..., - ) + if primaryChannelId != 0 { + loc := checkChannel(primaryChannelId, primaryLabel, primaryRequired) + if len(loc.missing) > 0 { + locations = append(locations, loc) + } } else { - // Channel mode permissions - requiredPermissions = append( - []permission.Permission{ - permission.ManageChannels, - }, - logic.StandardPermissions[:]..., - ) + missing := permissionwrapper.GetMissingPermissions(ctx.Worker(), ctx.GuildId(), ctx.Worker().BotId, primaryRequired...) + if len(missing) > 0 { + locations = append(locations, missingPermLocation{label: primaryLabel, missing: missing}) + } + } + + if panel != nil && panel.TicketNotificationChannel != nil { + loc := checkChannel(*panel.TicketNotificationChannel, "Notification channel", botpermissions.NotifChannelRequired) + if len(loc.missing) > 0 { + locations = append(locations, loc) + } } - if targetChannelId != 0 { - return permissionwrapper.GetMissingPermissionsChannel(ctx.Worker(), ctx.GuildId(), ctx.Worker().BotId, targetChannelId, requiredPermissions...), nil + if !useThreads && panel != nil && panel.OverflowEnabled { + if panel.OverflowCategoryId != nil { + loc := checkChannel(*panel.OverflowCategoryId, "Overflow category", botpermissions.ChannelModeRequired) + if len(loc.missing) > 0 { + locations = append(locations, loc) + } + } else if primaryChannelId != 0 { + // Skipped when the primary check was already guild-level + missing := permissionwrapper.GetMissingPermissions(ctx.Worker(), ctx.GuildId(), ctx.Worker().BotId, botpermissions.ChannelModeRequired...) + if len(missing) > 0 { + locations = append(locations, missingPermLocation{label: "Overflow (no category)", missing: missing}) + } + } + } + + if !useThreads && panel != nil && panel.PendingCategory != nil { + loc := checkChannel(*panel.PendingCategory, "Pending category", []permission.Permission{permission.ViewChannel, permission.ManageChannels}) + if len(loc.missing) > 0 { + locations = append(locations, loc) + } + } + + if panel != nil && panel.TranscriptChannelId != nil { + loc := checkChannel(*panel.TranscriptChannelId, "Transcript channel", botpermissions.TranscriptChannelRequired) + if len(loc.missing) > 0 { + locations = append(locations, loc) + } } - // If no target channel, just return guild-level missing permissions - return permissionwrapper.GetMissingPermissions(ctx.Worker(), ctx.GuildId(), ctx.Worker().BotId, requiredPermissions...), nil + return locations, nil } func formatTimestamp(seconds float64) string { diff --git a/bot/command/impl/admin/adminblacklist.go b/bot/command/impl/admin/adminblacklist.go index ec168574..59538747 100644 --- a/bot/command/impl/admin/adminblacklist.go +++ b/bot/command/impl/admin/adminblacklist.go @@ -62,7 +62,7 @@ func (AdminBlacklistCommand) Execute(ctx registry.CommandContext, guildIdRaw str if isBlacklisted, blacklistReason, _ := dbclient.Client.ServerBlacklist.IsBlacklisted(ctx, guildId); isBlacklisted { ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Orange, "Admin - Blacklist", @@ -112,7 +112,7 @@ func (AdminBlacklistCommand) Execute(ctx registry.CommandContext, guildIdRaw str } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Orange, "Admin - Blacklist", diff --git a/bot/command/impl/admin/admingenpremium.go b/bot/command/impl/admin/admingenpremium.go index 9907721c..e23eb669 100644 --- a/bot/command/impl/admin/admingenpremium.go +++ b/bot/command/impl/admin/admingenpremium.go @@ -110,7 +110,7 @@ func (AdminGenPremiumCommand) Execute(ctx registry.CommandContext, skuIdRaw stri } ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice( - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Orange, "Admin - Premium Key Generation", diff --git a/bot/command/impl/admin/adminlistuserentitlements.go b/bot/command/impl/admin/adminlistuserentitlements.go index 7792059d..282b7c28 100644 --- a/bot/command/impl/admin/adminlistuserentitlements.go +++ b/bot/command/impl/admin/adminlistuserentitlements.go @@ -77,7 +77,7 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents( + utils.BuildAdminContainerWithComponents( ctx, customisation.Orange, "Admin - User Entitlements", diff --git a/bot/command/impl/admin/adminrecache.go b/bot/command/impl/admin/adminrecache.go index 05812402..d350c35a 100644 --- a/bot/command/impl/admin/adminrecache.go +++ b/bot/command/impl/admin/adminrecache.go @@ -47,7 +47,7 @@ func (AdminRecacheCommand) Execute(ctx registry.CommandContext, guildIdRaw strin if onCooldown, cooldownTime := redis.GetRecacheCooldown(guildId); onCooldown { ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents( + utils.BuildAdminContainerWithComponents( ctx, customisation.Red, "Admin - Recache", @@ -91,7 +91,7 @@ func (AdminRecacheCommand) Execute(ctx registry.CommandContext, guildIdRaw strin } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents( + utils.BuildAdminContainerWithComponents( ctx, customisation.Orange, "Admin - Recache", diff --git a/bot/command/impl/admin/adminwhitelabelassignguild.go b/bot/command/impl/admin/adminwhitelabelassignguild.go index 3d7b266d..36877fba 100644 --- a/bot/command/impl/admin/adminwhitelabelassignguild.go +++ b/bot/command/impl/admin/adminwhitelabelassignguild.go @@ -69,7 +69,7 @@ func (AdminWhitelabelAssignGuildCommand) Execute(ctx registry.CommandContext, bo } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Orange, "Admin - Whitelabel Assign Guild", diff --git a/bot/command/impl/admin/adminwhitelabeldata.go b/bot/command/impl/admin/adminwhitelabeldata.go index bfeef989..d7bded05 100644 --- a/bot/command/impl/admin/adminwhitelabeldata.go +++ b/bot/command/impl/admin/adminwhitelabeldata.go @@ -140,7 +140,7 @@ func (AdminWhitelabelDataCommand) Execute(ctx registry.CommandContext, userId ui } components := []component.Component{ - utils.BuildContainerWithComponents(ctx, customisation.Green, "Admin - Whitelabel Data", innerComponents), + utils.BuildAdminContainerWithComponents(ctx, customisation.Green, "Admin - Whitelabel Data", innerComponents), } if data.BotId != 0 { diff --git a/bot/command/impl/admin/debug/debugserver.go b/bot/command/impl/admin/debug/debugserver.go index 34a860fd..f243b7fe 100644 --- a/bot/command/impl/admin/debug/debugserver.go +++ b/bot/command/impl/admin/debug/debugserver.go @@ -133,7 +133,7 @@ func (AdminDebugServerCommand) Execute(ctx registry.CommandContext, raw string) } } ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, customisation.Red, "Admin - This server is blacklisted", @@ -398,7 +398,7 @@ func (AdminDebugServerCommand) Execute(ctx registry.CommandContext, raw string) // Build component list components := []component.Component{ - utils.BuildContainerRaw( + utils.BuildAdminContainerRaw( ctx, colour, "Admin - Debug Server", diff --git a/bot/command/impl/tickets/remove.go b/bot/command/impl/tickets/remove.go index c21fe39b..42a42f48 100644 --- a/bot/command/impl/tickets/remove.go +++ b/bot/command/impl/tickets/remove.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/TicketsBot-cloud/common/botpermissions" permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/channel" "github.com/TicketsBot-cloud/gdl/objects/interaction" @@ -14,7 +15,6 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" - "github.com/TicketsBot-cloud/worker/bot/logic" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -238,7 +238,7 @@ func (RemoveCommand) Execute(ctx registry.CommandContext, id uint64) { Id: id, Type: channel.PermissionTypeMember, Allow: 0, - Deny: permission.BuildPermissions(logic.StandardPermissions[:]...), + Deny: permission.BuildPermissions(botpermissions.StandardPermissions...), } if err := ctx.Worker().EditChannelPermissions(reasonCtx, ticketChannelId, data); err != nil { @@ -322,7 +322,7 @@ func (RemoveCommand) Execute(ctx registry.CommandContext, id uint64) { Id: id, Type: channel.PermissionTypeRole, Allow: 0, - Deny: permission.BuildPermissions(logic.StandardPermissions[:]...), + Deny: permission.BuildPermissions(botpermissions.StandardPermissions...), } executor, err := ctx.Member() diff --git a/bot/logic/claim.go b/bot/logic/claim.go index bf29e41f..6f2827ff 100644 --- a/bot/logic/claim.go +++ b/bot/logic/claim.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/TicketsBot-cloud/common/botpermissions" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" @@ -218,7 +219,7 @@ func UnclaimTicket(ctx context.Context, cmd registry.CommandContext, ticket data overwrites = append(overwrites, channel.PermissionOverwrite{ Id: whoClaimed, Type: channel.PermissionTypeMember, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } @@ -377,7 +378,7 @@ func BuildClaimerOverwrite(ctx context.Context, worker *worker.Context, ticket d standard := channel.PermissionOverwrite{ Id: claimerId, Type: channel.PermissionTypeMember, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, } @@ -554,7 +555,7 @@ func overwritesCantView(claimerOverwrite channel.PermissionOverwrite, claimerId, overwrites = append(overwrites, channel.PermissionOverwrite{ Id: userId, Type: channel.PermissionTypeMember, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } @@ -563,7 +564,7 @@ func overwritesCantView(claimerOverwrite channel.PermissionOverwrite, claimerId, overwrites = append(overwrites, channel.PermissionOverwrite{ Id: roleId, Type: channel.PermissionTypeRole, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } @@ -609,7 +610,7 @@ func overwritesCantType(claimerOverwrite channel.PermissionOverwrite, claimerId, overwrites = append(overwrites, channel.PermissionOverwrite{ Id: userId, Type: channel.PermissionTypeMember, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } @@ -618,7 +619,7 @@ func overwritesCantType(claimerOverwrite channel.PermissionOverwrite, claimerId, overwrites = append(overwrites, channel.PermissionOverwrite{ Id: roleId, Type: channel.PermissionTypeRole, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } diff --git a/bot/logic/discordpermissions.go b/bot/logic/discordpermissions.go index 619a4349..2657a6d5 100644 --- a/bot/logic/discordpermissions.go +++ b/bot/logic/discordpermissions.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/TicketsBot-cloud/common/botpermissions" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel" "github.com/TicketsBot-cloud/gdl/permission" @@ -14,31 +15,8 @@ import ( "github.com/TicketsBot-cloud/worker/bot/utils" ) -// StandardPermissions Returns the standard permissions that users are given in a ticket -var StandardPermissions = [...]permission.Permission{ - permission.AddReactions, - permission.ViewChannel, - permission.SendMessages, - permission.SendTTSMessages, - permission.EmbedLinks, - permission.AttachFiles, - permission.MentionEveryone, - permission.UseExternalEmojis, - permission.ReadMessageHistory, - permission.UseApplicationCommands, - permission.UseExternalStickers, - permission.SendVoiceMessages, -} - -var MinimalPermissions = [...]permission.Permission{ - permission.ViewChannel, - permission.SendMessages, - permission.ReadMessageHistory, - permission.UseApplicationCommands, -} - func BuildUserOverwrite(userId uint64, additionalPermissions database.TicketPermissions) channel.PermissionOverwrite { - allow := MinimalPermissions[:] + allow := append([]permission.Permission{}, botpermissions.MinimalPermissions...) var deny []permission.Permission if additionalPermissions.AddReactions { @@ -92,7 +70,7 @@ func BuildUserOverwrite(userId uint64, additionalPermissions database.TicketPerm } func BuildRoleOverwrite(roleId uint64, additionalPermissions database.TicketPermissions) channel.PermissionOverwrite { - allow := MinimalPermissions[:] + allow := append([]permission.Permission{}, botpermissions.MinimalPermissions...) var deny []permission.Permission if additionalPermissions.AddReactions { diff --git a/bot/logic/open.go b/bot/logic/open.go index 9489aef5..22d492ed 100644 --- a/bot/logic/open.go +++ b/bot/logic/open.go @@ -9,6 +9,7 @@ import ( "time" "unicode" + "github.com/TicketsBot-cloud/common/botpermissions" "github.com/TicketsBot-cloud/common/featureflags" permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" @@ -170,7 +171,7 @@ func OpenTicket(ctx context.Context, cmd registry.InteractionContext, panel *dat if appPermissions != 0 && !permission.HasPermissionRaw(appPermissions, permission.Administrator) && !permission.HasPermissionRaw(appPermissions, permission.ViewChannel) { docsUrl := fmt.Sprintf("%s/miscellaneous/permissions-explained", config.Conf.Bot.DocsUrl) message := cmd.GetMessage(i18n.MessageErrorMissingPermissionsTitle) + ":\n" - message += "* `View Channel`\n" + message += fmt.Sprintf("* `%s`\n", permission.ViewChannel.ChannelName(panelChannel.Type)) message += "\n" + cmd.GetMessage(i18n.MessageErrorMissingPermissionsBody, docsUrl) cmd.ReplyRaw(customisation.Red, cmd.GetMessage(i18n.Error), message) @@ -994,15 +995,14 @@ func CreateOverwrites(ctx context.Context, cmd registry.CommandContext, userId u } // Add the bot to the overwrites - selfAllow := make([]permission.Permission, len(StandardPermissions), len(StandardPermissions)+2) - copy(selfAllow, StandardPermissions[:]) // Do not append to StandardPermissions + selfAllow := append([]permission.Permission{}, botpermissions.StandardPermissions...) selfAllow = append(selfAllow, permission.ManageChannels) // Only add PinMessages if the bot has the permission if permissionwrapper.HasPermissions(cmd.Worker(), cmd.GuildId(), cmd.Worker().BotId, permission.PinMessages) { selfAllow = append(selfAllow, permission.PinMessages) - } else if permissionwrapper.HasPermissionsChannel(cmd.Worker(), cmd.GuildId(), cmd.ChannelId(), cmd.Worker().BotId, permission.PinMessages) { + } else if permissionwrapper.HasPermissionsChannel(cmd.Worker(), cmd.GuildId(), cmd.Worker().BotId, categoryId, permission.PinMessages) { selfAllow = append(selfAllow, permission.PinMessages) } @@ -1051,8 +1051,7 @@ func CreateOverwrites(ctx context.Context, cmd registry.CommandContext, userId u continue // Already added overwrite above } - allow := make([]permission.Permission, len(StandardPermissions)) - copy(allow, StandardPermissions[:]) + allow := append([]permission.Permission{}, botpermissions.StandardPermissions...) overwrites = append(overwrites, channel.PermissionOverwrite{ Id: member, @@ -1066,7 +1065,7 @@ func CreateOverwrites(ctx context.Context, cmd registry.CommandContext, userId u overwrites = append(overwrites, channel.PermissionOverwrite{ Id: role, Type: channel.PermissionTypeRole, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } @@ -1416,8 +1415,7 @@ func countRealChannels(channels []channel.Channel, parentId uint64) int { var count int for _, ch := range channels { - // Ignore threads - if ch.Type == channel.ChannelTypeGuildPublicThread || ch.Type == channel.ChannelTypeGuildPrivateThread || ch.Type == channel.ChannelTypeGuildNewsThread { + if ch.Type.IsThread() { continue } diff --git a/bot/permissionwrapper/permission.go b/bot/permissionwrapper/permission.go index daff2164..d69ba001 100644 --- a/bot/permissionwrapper/permission.go +++ b/bot/permissionwrapper/permission.go @@ -3,325 +3,118 @@ package permissionwrapper import ( "errors" + "github.com/TicketsBot-cloud/common/botpermissions" "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/gdl/objects/channel" "github.com/TicketsBot-cloud/gdl/objects/guild" + "github.com/TicketsBot-cloud/gdl/objects/member" "github.com/TicketsBot-cloud/gdl/permission" "github.com/TicketsBot-cloud/worker" + "github.com/TicketsBot-cloud/worker/bot/utils" ) var ErrChannelObfuscated = errors.New("channel metadata is obfuscated") func HasPermissionsChannel(ctx *worker.Context, guildId, userId, channelId uint64, permissions ...permission.Permission) bool { - sum, err := getEffectivePermissionsChannel(ctx, guildId, userId, channelId) + m, roles, ch, err := fetchChannelData(ctx, guildId, userId, channelId) if err != nil { + // Administrator outranks anything the channel could say, including being unreadable + if errors.Is(err, ErrChannelObfuscated) { + return HasPermissions(ctx, guildId, userId, permission.Administrator) + } + + sentry.Error(err) return false } - - if permission.HasPermissionRaw(sum, permission.Administrator) { + effective := botpermissions.EffectivePermissions(guildId, userId, []uint64(m.Roles), ch.PermissionOverwrites, buildRoleMap(roles)) + if permission.HasPermissionRaw(effective, permission.Administrator) { return true } - - hasPermission := true - - for _, perm := range permissions { - if !permission.HasPermissionRaw(sum, perm) { - hasPermission = false - break + for _, p := range permissions { + if !permission.HasPermissionRaw(effective, p) { + return false } } - - return hasPermission + return true } func HasPermissions(ctx *worker.Context, guildId, userId uint64, permissions ...permission.Permission) bool { - sum, err := getEffectivePermissions(ctx, guildId, userId) + m, roles, err := fetchGuildData(ctx, guildId, userId) if err != nil { sentry.Error(err) return false } - - if permission.HasPermissionRaw(sum, permission.Administrator) { + effective := botpermissions.EffectivePermissions(guildId, userId, []uint64(m.Roles), nil, buildRoleMap(roles)) + if permission.HasPermissionRaw(effective, permission.Administrator) { return true } - - hasPermission := true - - for _, perm := range permissions { - if !permission.HasPermissionRaw(sum, perm) { - hasPermission = false - break - } - } - - return hasPermission -} - -func getAllPermissionsChannel(ctx *worker.Context, guildId, userId, channelId uint64) []permission.Permission { - permissions := make([]permission.Permission, 0) - - sum, err := getEffectivePermissionsChannel(ctx, guildId, userId, channelId) - if err != nil { - if !errors.Is(err, ErrChannelObfuscated) { - sentry.Error(err) - } - - return permissions - } - - for _, perm := range permission.AllPermissions { - if permission.HasPermissionRaw(sum, perm) { - permissions = append(permissions, perm) - } - } - - return permissions -} - -func getAllPermissions(ctx *worker.Context, guildId, userId uint64) []permission.Permission { - permissions := make([]permission.Permission, 0) - - sum, err := getEffectivePermissions(ctx, guildId, userId) - if err != nil { - sentry.Error(err) - return permissions - } - - for _, perm := range permission.AllPermissions { - if permission.HasPermissionRaw(sum, perm) { - permissions = append(permissions, perm) + for _, p := range permissions { + if !permission.HasPermissionRaw(effective, p) { + return false } } - - return permissions + return true } func GetMissingPermissions(ctx *worker.Context, guildId, userId uint64, required ...permission.Permission) []permission.Permission { - missing := make([]permission.Permission, 0) - - sum, err := getEffectivePermissions(ctx, guildId, userId) + m, roles, err := fetchGuildData(ctx, guildId, userId) if err != nil { sentry.Error(err) return required } - - if permission.HasPermissionRaw(sum, permission.Administrator) { - return missing - } - - for _, perm := range required { - if !permission.HasPermissionRaw(sum, perm) { - missing = append(missing, perm) - } - } - - return missing + return botpermissions.MissingPermissions(guildId, userId, []uint64(m.Roles), nil, buildRoleMap(roles), required) } -func GetMissingPermissionsChannel(ctx *worker.Context, guildId, userId, channelId uint64, required ...permission.Permission) []permission.Permission { - missing := make([]permission.Permission, 0) - - sum, err := getEffectivePermissionsChannel(ctx, guildId, userId, channelId) +func GetMissingPermissionsChannel(ctx *worker.Context, guildId, userId, channelId uint64, required ...permission.Permission) ([]permission.Permission, channel.Channel, error) { + m, roles, ch, err := fetchChannelData(ctx, guildId, userId, channelId) if err != nil { if errors.Is(err, ErrChannelObfuscated) { - return []permission.Permission{permission.ViewChannel} + return []permission.Permission{permission.ViewChannel}, ch, err } sentry.Error(err) - return required - } - - if permission.HasPermissionRaw(sum, permission.Administrator) { - return missing + return required, channel.Channel{}, err } - - for _, perm := range required { - if !permission.HasPermissionRaw(sum, perm) { - missing = append(missing, perm) - } - } - - return missing + missing := botpermissions.MissingPermissions(guildId, userId, []uint64(m.Roles), ch.PermissionOverwrites, buildRoleMap(roles), required) + return missing, ch, nil } -func getEffectivePermissionsChannel(ctx *worker.Context, guildId, userId, channelId uint64) (uint64, error) { - permissions, err := getBasePermissions(ctx, guildId) +func fetchGuildData(ctx *worker.Context, guildId, userId uint64) (member.Member, []guild.Role, error) { + m, err := ctx.GetGuildMember(guildId, userId) if err != nil { - return 0, err + return member.Member{}, nil, err } - - permissions, err = getGuildTotalRolePermissions(ctx, guildId, userId, permissions) - if err != nil { - return 0, err - } - - if permission.HasPermissionRaw(permissions, permission.Administrator) { - return permissions, nil - } - - permissions, err = getChannelBasePermissions(ctx, guildId, channelId, permissions) - if err != nil { - return 0, err - } - - permissions, err = getChannelTotalRolePermissions(ctx, guildId, userId, channelId, permissions) - if err != nil { - return 0, err - } - - permissions, err = getChannelMemberPermissions(ctx, userId, channelId, permissions) - if err != nil { - return 0, err - } - - return permissions, nil -} - -func getEffectivePermissions(ctx *worker.Context, guildId, userId uint64) (uint64, error) { - permissions, err := getBasePermissions(ctx, guildId) - if err != nil { - return 0, err - } - - permissions, err = getGuildTotalRolePermissions(ctx, guildId, userId, permissions) - if err != nil { - return 0, err - } - - return permissions, nil -} - -func getChannelMemberPermissions(ctx *worker.Context, userId, channelId uint64, initialPermissions uint64) (uint64, error) { - ch, err := ctx.GetChannel(channelId) - if err != nil { - return 0, err - } - - for _, overwrite := range ch.PermissionOverwrites { - if overwrite.Type == channel.PermissionTypeMember && overwrite.Id == userId { - initialPermissions &= ^overwrite.Deny - initialPermissions |= overwrite.Allow - } - } - - return initialPermissions, nil -} - -func getChannelTotalRolePermissions(ctx *worker.Context, guildId, userId, channelId uint64, initialPermissions uint64) (uint64, error) { - member, err := ctx.GetGuildMember(guildId, userId) - if err != nil { - return 0, err - } - roles, err := ctx.GetGuildRoles(guildId) if err != nil { - return 0, err - } - - ch, err := ctx.GetChannel(channelId) - if err != nil { - return 0, err - } - - var allow, deny uint64 - - for _, memberRole := range member.Roles { - for _, role := range roles { - if memberRole == role.Id { - for _, overwrite := range ch.PermissionOverwrites { - if overwrite.Type == channel.PermissionTypeRole && overwrite.Id == role.Id { - allow |= overwrite.Allow - deny |= overwrite.Deny - break - } - } - } - } + return member.Member{}, nil, err } - - initialPermissions &= ^deny - initialPermissions |= allow - - return initialPermissions, nil + return m, roles, nil } -func getChannelBasePermissions(ctx *worker.Context, guildId, channelId uint64, initialPermissions uint64) (uint64, error) { - roles, err := ctx.GetGuildRoles(guildId) - if err != nil { - return 0, err - } - - var publicRole *guild.Role - for _, role := range roles { - if role.Id == guildId { - publicRole = &role - break - } - } - - if publicRole == nil { - return 0, errors.New("couldn't find public role") - } - +func fetchChannelData(ctx *worker.Context, guildId, userId, channelId uint64) (member.Member, []guild.Role, channel.Channel, error) { + // Before the guild data, so an unreadable channel isn't masked by a member/role lookup failure ch, err := ctx.GetChannel(channelId) if err != nil { - return 0, err - } - - if ch.IsObfuscated() { - return 0, ErrChannelObfuscated - } - - for _, overwrite := range ch.PermissionOverwrites { - if overwrite.Type == channel.PermissionTypeRole && overwrite.Id == publicRole.Id { - initialPermissions &= ^overwrite.Deny - initialPermissions |= overwrite.Allow - break + if utils.IsMissingAccess(err) { + return member.Member{}, nil, channel.Channel{}, ErrChannelObfuscated } + return member.Member{}, nil, channel.Channel{}, err } - - return initialPermissions, nil -} - -func getGuildTotalRolePermissions(ctx *worker.Context, guildId, userId uint64, initialPermissions uint64) (uint64, error) { - member, err := ctx.GetGuildMember(guildId, userId) - if err != nil { - return 0, err + if ch.IsObfuscated() { + return member.Member{}, nil, ch, ErrChannelObfuscated } - roles, err := ctx.GetGuildRoles(guildId) + m, roles, err := fetchGuildData(ctx, guildId, userId) if err != nil { - return 0, err - } - - for _, memberRole := range member.Roles { - for _, role := range roles { - if memberRole == role.Id { - initialPermissions |= role.Permissions - } - } + return member.Member{}, nil, channel.Channel{}, err } - - return initialPermissions, nil + return m, roles, ch, nil } -func getBasePermissions(ctx *worker.Context, guildId uint64) (uint64, error) { - roles, err := ctx.GetGuildRoles(guildId) - if err != nil { - return 0, err - } - - var publicRole *guild.Role - for _, role := range roles { - if role.Id == guildId { - publicRole = &role - break - } +func buildRoleMap(roles []guild.Role) map[uint64]guild.Role { + m := make(map[uint64]guild.Role, len(roles)) + for _, r := range roles { + m[r.Id] = r } - - if publicRole == nil { - return 0, errors.New("couldn't find public role") - } - - return publicRole.Permissions, nil + return m } diff --git a/bot/utils/component_utils.go b/bot/utils/component_utils.go index a836ecb8..d947eabd 100644 --- a/bot/utils/component_utils.go +++ b/bot/utils/component_utils.go @@ -29,22 +29,39 @@ func BuildContainerRaw(ctx registry.CommandContext, colour customisation.Colour, } func BuildContainerWithComponents[T string | i18n.MessageId](ctx registry.CommandContext, colour customisation.Colour, title T, innerComponents []component.Component) component.Component { - var titleStr string + return buildContainer(ctx, colour, containerTitle(ctx, title), innerComponents, true) +} + +func BuildAdminContainerRaw(ctx registry.CommandContext, colour customisation.Colour, title, content string) component.Component { + return BuildAdminContainerWithComponents(ctx, colour, title, Slice(component.BuildTextDisplay(component.TextDisplay{ + Content: content, + }))) +} + +func BuildAdminContainerWithComponents[T string | i18n.MessageId](ctx registry.CommandContext, colour customisation.Colour, title T, innerComponents []component.Component) component.Component { + return buildContainer(ctx, colour, containerTitle(ctx, title), innerComponents, false) +} + +func containerTitle[T string | i18n.MessageId](ctx registry.CommandContext, title T) string { switch t := any(title).(type) { case string: - titleStr = t + return t case i18n.MessageId: - titleStr = ctx.GetMessage(t) + return ctx.GetMessage(t) } + return "" +} + +func buildContainer(ctx registry.CommandContext, colour customisation.Colour, title string, innerComponents []component.Component, branded bool) component.Component { components := append(Slice( component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("### %s", titleStr), + Content: fmt.Sprintf("### %s", title), }), component.BuildSeparator(component.Separator{}), ), innerComponents...) - if ctx.PremiumTier() == premium.None && !ctx.Worker().IsWhitelabel { + if branded && ctx.PremiumTier() == premium.None && !ctx.Worker().IsWhitelabel { components = addPremiumFooter(components) }