From f573f8870107e538eca2324692f303a358b4f60a Mon Sep 17 00:00:00 2001 From: biast12 Date: Sat, 11 Apr 2026 17:10:23 +0200 Subject: [PATCH 1/4] Refactor permission checks for thread/channel modes Make permission checking mode-aware and group missing perms by location. - Adjust server-wide permission list to depend on settings.UseThreads to avoid reporting irrelevant missing permissions. Append standard permissions after mode-specific ones. - In panel permission checks, treat a panel as using threads if either global settings or the panel enable threads. Use logic.ThreadPermissions for thread-mode channel checks and simplify assembled permission slices. - Add ThreadPermissions constant in bot/logic/discordpermissions.go for reuse. - Rewrite findMissingPermissions to return labeled missing-permission groups (panel channel, notification channel, transcript channel) instead of a flat list; check primary channel, optional notification channel (thread mode), and transcript channel, using channel-level and guild-level checks as appropriate. - Update reply formatting to render missing permissions grouped by location with a title and footer linking docs. These changes reduce false positives, centralize thread-mode permissions, and provide clearer error messages to users about where permissions are missing. --- .../admindebug/server/modals/permissions.go | 45 ++++--- bot/command/context/replyable.go | 114 +++++++++++------- bot/logic/discordpermissions.go | 12 ++ 3 files changed, 105 insertions(+), 66 deletions(-) diff --git a/bot/button/handlers/admindebug/server/modals/permissions.go b/bot/button/handlers/admindebug/server/modals/permissions.go index 1bd2609..c625038 100644 --- a/bot/button/handlers/admindebug/server/modals/permissions.go +++ b/bot/button/handlers/admindebug/server/modals/permissions.go @@ -152,23 +152,25 @@ func (h *AdminDebugServerPermissionsModalSubmitHandler) Execute(ctx *context.Mod } func processPermissionChecks(selectedValues []string, worker *w.Context, guildId uint64, botMember member.Member, settings database.Settings, panels []database.Panel) ([]string, bool) { - // Server-wide permissions - serverWidePermissions := append( - []permission.Permission{ - // Thread mode specific + // Server-wide permissions depend on the active mode so we don't report + // false failures for permissions that are irrelevant to the current mode + serverWidePermissions := []permission.Permission{ + // Required in both modes + permission.ManageWebhooks, + permission.PinMessages, + // Server-wide only + permission.ManageRoles, + } + if settings.UseThreads { + serverWidePermissions = append(serverWidePermissions, permission.CreatePrivateThreads, permission.SendMessagesInThreads, permission.ManageThreads, - // Channel mode specific - permission.ManageChannels, - // Both modes - permission.ManageWebhooks, - permission.PinMessages, - // Server-wide only - permission.ManageRoles, - }, - logic.StandardPermissions[:]..., - ) + ) + } else { + serverWidePermissions = append(serverWidePermissions, permission.ManageChannels) + } + serverWidePermissions = append(serverWidePermissions, logic.StandardPermissions[:]...) var results []string var hasMissingPermissions bool @@ -256,22 +258,17 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M var hasMissingPermissions bool // Determine if this panel uses threads or channels - usesThreads := panel.UseThreads + usesThreads := settings.UseThreads || panel.UseThreads // Check panel channel permissions (if panel has a channel) if panel.ChannelId != 0 { var panelChannelPerms []permission.Permission if usesThreads { - // Thread mode: specific thread permissions + standard permissions + // Thread mode: permissions needed in the panel channel where threads are created. panelChannelPerms = append( - []permission.Permission{ - permission.CreatePrivateThreads, - permission.SendMessagesInThreads, - permission.ManageThreads, - permission.ManageWebhooks, - permission.PinMessages, - }, - logic.StandardPermissions[:]..., + logic.ThreadPermissions[:], + permission.ManageWebhooks, + permission.PinMessages, ) } else { // Channel mode: just standard permissions (no special ones needed for panel channel in channel mode) diff --git a/bot/command/context/replyable.go b/bot/command/context/replyable.go index 0e14f07..88a5659 100644 --- a/bot/command/context/replyable.go +++ b/bot/command/context/replyable.go @@ -212,15 +212,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", perm.String()) + } } - message += "\n" + r.GetMessage(i18n.MessageErrorMissingPermissionsBody, docsUrl) + message = title + locationMsg + footer } else { message = r.GetMessage(i18n.MessageErrorMissingAccess, docsUrl) } @@ -308,16 +314,19 @@ 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 +} + +func findMissingPermissions(ctx registry.InteractionContext) ([]missingPermLocation, error) { if permission.HasPermissionRaw(ctx.InteractionMetadata().AppPermissions, permission.Administrator) { return nil, nil } var useThreads bool - var targetChannelId uint64 - - settings, err := ctx.Settings() - if err == nil { + settings, settingsErr := ctx.Settings() + if settingsErr == nil { useThreads = settings.UseThreads } @@ -326,55 +335,76 @@ func findMissingPermissions(ctx registry.InteractionContext) ([]permission.Permi p, panelExists, err := dbclient.Client.Panel.GetByCustomId(context.Background(), ctx.GuildId(), btnCtx.InteractionData.CustomId) if err == nil && panelExists { panel = &p - // Panel can enable threads if global setting is disabled if !useThreads { useThreads = panel.UseThreads } } } + checkChannel := func(channelId uint64, label string, required []permission.Permission) missingPermLocation { + missing := permissionwrapper.GetMissingPermissionsChannel(ctx.Worker(), ctx.GuildId(), ctx.Worker().BotId, channelId, required...) + return missingPermLocation{label: label, missing: missing} + } + + var locations []missingPermLocation + + // 1. Primary location: panel channel (thread mode) or ticket category (channel mode) + 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 = logic.ThreadPermissions[:] } else { - // Channel mode - check permissions in the ticket category + primaryLabel = "Ticket category" + primaryRequired = append([]permission.Permission{permission.ManageChannels}, logic.StandardPermissions[:]...) if panel != nil && panel.TargetCategory != 0 { - // Use panel's target category - targetChannelId = panel.TargetCategory + primaryChannelId = panel.TargetCategory } else { - // Fall back to guild default category - targetChannelId, _ = dbclient.Client.ChannelCategory.Get(context.Background(), ctx.GuildId()) + primaryChannelId, _ = dbclient.Client.ChannelCategory.Get(context.Background(), ctx.GuildId()) } } - // 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}) + } + } + + // 2. Notification channel (thread mode only) + if useThreads { + var notifChannelId *uint64 + if panel != nil && panel.TicketNotificationChannel != nil { + notifChannelId = panel.TicketNotificationChannel + } else if settingsErr == nil && settings.TicketNotificationChannel != nil { + notifChannelId = settings.TicketNotificationChannel + } + if notifChannelId != nil { + notifRequired := append(logic.MinimalPermissions[:], permission.EmbedLinks, permission.AttachFiles) + loc := checkChannel(*notifChannelId, "Notification channel", notifRequired) + 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 + // 3. Transcript channel (panel-level, both modes) + if panel != nil && panel.TranscriptChannelId != nil { + loc := checkChannel(*panel.TranscriptChannelId, "Transcript channel", logic.MinimalPermissions[:]) + 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/logic/discordpermissions.go b/bot/logic/discordpermissions.go index 619a434..fd22fb7 100644 --- a/bot/logic/discordpermissions.go +++ b/bot/logic/discordpermissions.go @@ -37,6 +37,18 @@ var MinimalPermissions = [...]permission.Permission{ permission.UseApplicationCommands, } +// ThreadPermissions are the permissions the bot needs in the panel channel when operating in thread mode +var ThreadPermissions = [...]permission.Permission{ + permission.ViewChannel, + permission.ReadMessageHistory, + permission.EmbedLinks, + permission.AttachFiles, + permission.UseExternalEmojis, + permission.CreatePrivateThreads, + permission.SendMessagesInThreads, + permission.ManageThreads, +} + func BuildUserOverwrite(userId uint64, additionalPermissions database.TicketPermissions) channel.PermissionOverwrite { allow := MinimalPermissions[:] var deny []permission.Permission From baf5436de303e27b8fd57e5b53014e77d1b23237 Mon Sep 17 00:00:00 2001 From: biast12 Date: Sun, 12 Apr 2026 12:48:43 +0200 Subject: [PATCH 2/4] Switch to common botpermissions package Replace local permission constants/usages with the centralized github.com/TicketsBot-cloud/common/botpermissions package across the codebase. Updated imports and calls to BuildPermissions to use botpermissions.StandardPermissions, MinimalPermissions, ThreadModeRequired, NotifChannelRequired, TranscriptChannelRequired, and ChannelModeRequired where appropriate. Removed duplicate permission arrays from bot/logic/discordpermissions.go and adjusted code to copy/append permission slices safely. Also enabled a local replace for the common module in go.mod. This centralizes permission definitions and reduces duplication. --- bot/button/handlers/addadmin.go | 4 +- bot/button/handlers/addsupport.go | 4 +- .../admindebug/server/modals/permissions.go | 14 +++---- bot/button/handlers/unclaim.go | 3 +- bot/command/context/replyable.go | 10 ++--- bot/command/impl/tickets/remove.go | 6 +-- bot/command/impl/tickets/unclaim.go | 3 +- bot/logic/claim.go | 9 +++-- bot/logic/discordpermissions.go | 40 ++----------------- bot/logic/open.go | 9 ++--- go.mod | 2 +- 11 files changed, 36 insertions(+), 68 deletions(-) diff --git a/bot/button/handlers/addadmin.go b/bot/button/handlers/addadmin.go index 13b31c2..d3bb911 100644 --- a/bot/button/handlers/addadmin.go +++ b/bot/button/handlers/addadmin.go @@ -19,7 +19,7 @@ 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/common/botpermissions" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -210,7 +210,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 39b3d84..c3949ef 100644 --- a/bot/button/handlers/addsupport.go +++ b/bot/button/handlers/addsupport.go @@ -21,7 +21,7 @@ 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/common/botpermissions" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -235,7 +235,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/modals/permissions.go b/bot/button/handlers/admindebug/server/modals/permissions.go index c625038..5b524ef 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" ) @@ -170,7 +170,7 @@ func processPermissionChecks(selectedValues []string, worker *w.Context, guildId } else { serverWidePermissions = append(serverWidePermissions, permission.ManageChannels) } - serverWidePermissions = append(serverWidePermissions, logic.StandardPermissions[:]...) + serverWidePermissions = append(serverWidePermissions, botpermissions.StandardPermissions...) var results []string var hasMissingPermissions bool @@ -266,13 +266,13 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M if usesThreads { // Thread mode: permissions needed in the panel channel where threads are created. panelChannelPerms = append( - logic.ThreadPermissions[:], + botpermissions.ThreadModeRequired, permission.ManageWebhooks, permission.PinMessages, ) } else { // Channel mode: just standard permissions (no special ones needed for panel channel in channel mode) - panelChannelPerms = append([]permission.Permission{}, logic.StandardPermissions[:]...) + panelChannelPerms = append([]permission.Permission{}, botpermissions.StandardPermissions...) } result, hasMissing := checkChannelPermissions(worker, panel.ChannelId, botMember, guildId, panelChannelPerms, "Panel Channel") results = append(results, result) @@ -290,7 +290,7 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M permission.ManageWebhooks, permission.PinMessages, }, - logic.StandardPermissions[:]..., + botpermissions.StandardPermissions..., ) result, hasMissing := checkChannelPermissions(worker, panel.TargetCategory, botMember, guildId, categoryPerms, "Category") results = append(results, result) @@ -302,7 +302,7 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M // 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 := append([]permission.Permission{}, botpermissions.MinimalPermissions...) result, hasMissing := checkChannelPermissions(worker, *panel.TranscriptChannelId, botMember, guildId, transcriptPerms, "Transcript Channel") results = append(results, result) if hasMissing { @@ -318,7 +318,7 @@ func checkPanelPermissions(worker *w.Context, guildId uint64, botMember member.M permission.EmbedLinks, permission.AttachFiles, }, - logic.MinimalPermissions[:]..., + botpermissions.MinimalPermissions..., ) result, hasMissing := checkChannelPermissions(worker, *settings.TicketNotificationChannel, botMember, guildId, notificationPerms, "Notification Channel") results = append(results, result) diff --git a/bot/button/handlers/unclaim.go b/bot/button/handlers/unclaim.go index 3c37347..407cbf3 100644 --- a/bot/button/handlers/unclaim.go +++ b/bot/button/handlers/unclaim.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" + "github.com/TicketsBot-cloud/common/botpermissions" "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel" @@ -148,7 +149,7 @@ func (h *UnclaimHandler) Execute(ctx *context.ButtonContext) { overwrites = append(overwrites, channel.PermissionOverwrite{ Id: whoClaimed, Type: channel.PermissionTypeMember, - Allow: discordpermission.BuildPermissions(logic.StandardPermissions[:]...), + Allow: discordpermission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) case database.SwitchPanelRemoveOnUnclaim: diff --git a/bot/command/context/replyable.go b/bot/command/context/replyable.go index 88a5659..cc473e0 100644 --- a/bot/command/context/replyable.go +++ b/bot/command/context/replyable.go @@ -9,6 +9,7 @@ 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" @@ -22,7 +23,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/permissionwrapper" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/config" @@ -356,10 +356,10 @@ func findMissingPermissions(ctx registry.InteractionContext) ([]missingPermLocat if useThreads { primaryChannelId = ctx.ChannelId() primaryLabel = "Panel channel" - primaryRequired = logic.ThreadPermissions[:] + primaryRequired = botpermissions.ThreadModeRequired } else { primaryLabel = "Ticket category" - primaryRequired = append([]permission.Permission{permission.ManageChannels}, logic.StandardPermissions[:]...) + primaryRequired = botpermissions.ChannelModeRequired if panel != nil && panel.TargetCategory != 0 { primaryChannelId = panel.TargetCategory } else { @@ -388,7 +388,7 @@ func findMissingPermissions(ctx registry.InteractionContext) ([]missingPermLocat notifChannelId = settings.TicketNotificationChannel } if notifChannelId != nil { - notifRequired := append(logic.MinimalPermissions[:], permission.EmbedLinks, permission.AttachFiles) + notifRequired := botpermissions.NotifChannelRequired loc := checkChannel(*notifChannelId, "Notification channel", notifRequired) if len(loc.missing) > 0 { locations = append(locations, loc) @@ -398,7 +398,7 @@ func findMissingPermissions(ctx registry.InteractionContext) ([]missingPermLocat // 3. Transcript channel (panel-level, both modes) if panel != nil && panel.TranscriptChannelId != nil { - loc := checkChannel(*panel.TranscriptChannelId, "Transcript channel", logic.MinimalPermissions[:]) + loc := checkChannel(*panel.TranscriptChannelId, "Transcript channel", botpermissions.TranscriptChannelRequired) if len(loc.missing) > 0 { locations = append(locations, loc) } diff --git a/bot/command/impl/tickets/remove.go b/bot/command/impl/tickets/remove.go index c21fe39..40d74be 100644 --- a/bot/command/impl/tickets/remove.go +++ b/bot/command/impl/tickets/remove.go @@ -14,7 +14,7 @@ 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/common/botpermissions" "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/command/impl/tickets/unclaim.go b/bot/command/impl/tickets/unclaim.go index dd7906c..e5971e6 100644 --- a/bot/command/impl/tickets/unclaim.go +++ b/bot/command/impl/tickets/unclaim.go @@ -3,6 +3,7 @@ package tickets import ( "fmt" + "github.com/TicketsBot-cloud/common/botpermissions" "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel" @@ -144,7 +145,7 @@ func (UnclaimCommand) Execute(ctx *context.SlashCommandContext) { overwrites = append(overwrites, channel.PermissionOverwrite{ Id: whoClaimed, Type: channel.PermissionTypeMember, - Allow: discordpermission.BuildPermissions(logic.StandardPermissions[:]...), + Allow: discordpermission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) case database.SwitchPanelRemoveOnUnclaim: diff --git a/bot/logic/claim.go b/bot/logic/claim.go index a580ad1..3d14698 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" @@ -219,7 +220,7 @@ func overwritesCantView(claimer, selfId, openerId, guildId uint64, adminUsers, a overwrites = append(overwrites, channel.PermissionOverwrite{ Id: userId, Type: channel.PermissionTypeMember, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } @@ -228,7 +229,7 @@ func overwritesCantView(claimer, selfId, openerId, guildId uint64, adminUsers, a overwrites = append(overwrites, channel.PermissionOverwrite{ Id: roleId, Type: channel.PermissionTypeRole, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } @@ -269,7 +270,7 @@ func overwritesCantType(claimerId, selfId, openerId, guildId uint64, supportUser overwrites = append(overwrites, channel.PermissionOverwrite{ Id: userId, Type: channel.PermissionTypeMember, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } @@ -278,7 +279,7 @@ func overwritesCantType(claimerId, selfId, openerId, guildId uint64, supportUser 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 fd22fb7..2657a6d 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,43 +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, -} - -// ThreadPermissions are the permissions the bot needs in the panel channel when operating in thread mode -var ThreadPermissions = [...]permission.Permission{ - permission.ViewChannel, - permission.ReadMessageHistory, - permission.EmbedLinks, - permission.AttachFiles, - permission.UseExternalEmojis, - permission.CreatePrivateThreads, - permission.SendMessagesInThreads, - permission.ManageThreads, -} - func BuildUserOverwrite(userId uint64, additionalPermissions database.TicketPermissions) channel.PermissionOverwrite { - allow := MinimalPermissions[:] + allow := append([]permission.Permission{}, botpermissions.MinimalPermissions...) var deny []permission.Permission if additionalPermissions.AddReactions { @@ -104,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 ccdb3aa..fcf41cd 100644 --- a/bot/logic/open.go +++ b/bot/logic/open.go @@ -9,6 +9,7 @@ import ( "time" "unicode" + "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" @@ -941,8 +942,7 @@ func CreateOverwrites(ctx context.Context, cmd registry.InteractionContext, user } // 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) @@ -998,8 +998,7 @@ func CreateOverwrites(ctx context.Context, cmd registry.InteractionContext, user 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, @@ -1013,7 +1012,7 @@ func CreateOverwrites(ctx context.Context, cmd registry.InteractionContext, user overwrites = append(overwrites, channel.PermissionOverwrite{ Id: role, Type: channel.PermissionTypeRole, - Allow: permission.BuildPermissions(StandardPermissions[:]...), + Allow: permission.BuildPermissions(botpermissions.StandardPermissions...), Deny: 0, }) } diff --git a/go.mod b/go.mod index f3bc8de..fdb5574 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.2 //replace github.com/TicketsBot-cloud/database => ../database -//replace github.com/TicketsBot-cloud/common => ../common +replace github.com/TicketsBot-cloud/common => ../common //replace github.com/TicketsBot-cloud/gdl => ../gdl From c758476d5f9e5906e53ef53276c14e93481386e2 Mon Sep 17 00:00:00 2001 From: biast12 Date: Sun, 12 Apr 2026 20:40:24 +0200 Subject: [PATCH 3/4] Bump common --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index fdb5574..f7473bc 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.2 //replace github.com/TicketsBot-cloud/database => ../database -replace github.com/TicketsBot-cloud/common => ../common +//replace github.com/TicketsBot-cloud/common => ../common //replace github.com/TicketsBot-cloud/gdl => ../gdl @@ -18,7 +18,7 @@ require ( cloud.google.com/go/profiler v0.4.2 github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704 - github.com/TicketsBot-cloud/common v0.0.0-20260210203202-54154661338e + github.com/TicketsBot-cloud/common v0.0.0-20260412182419-83b9a6ea08e7 github.com/TicketsBot-cloud/database v0.0.0-20260308193919-30a698fefa8b github.com/TicketsBot-cloud/gdl v0.0.0-20260306134952-cccb0116fef6 github.com/caarlos0/env/v10 v10.0.0 diff --git a/go.sum b/go.sum index 380325f..e2f102b 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c/go.mod h1:zecIz09jVDSHyhV6NYgTko0NEN0QJGiZbzcxHRjQLzc= github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704 h1:liLfvCrzoJ89DXFHzsd1iK3cyP8s4i0CnZPRFEj53zg= github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704/go.mod h1:Mux1bEPpOHwRw1wo6Fa6qJLJH9Erk9qv1yAIfLi1Wmw= -github.com/TicketsBot-cloud/common v0.0.0-20260210203202-54154661338e h1:nFKV7yEm8MWbCP7dtsJ88+agcxDUD0YKIotVHMVvytw= -github.com/TicketsBot-cloud/common v0.0.0-20260210203202-54154661338e/go.mod h1:tGrTHFz09OM3eDWF+62hIi9ELpT4igCFi868FKSvKBg= +github.com/TicketsBot-cloud/common v0.0.0-20260412182419-83b9a6ea08e7 h1:dFmPLk9KXGRVSfiuKK6kusVhRKG7nGfiGld+WRuiX7w= +github.com/TicketsBot-cloud/common v0.0.0-20260412182419-83b9a6ea08e7/go.mod h1:jXGcmAuRvv92YqITskvClgoCpFVqYw14CKJdYhiLtVU= github.com/TicketsBot-cloud/database v0.0.0-20260308193919-30a698fefa8b h1:bHkfJWo8T/9TiHuYHxaOz8GAILIiKPugC1k3CzdOq/A= github.com/TicketsBot-cloud/database v0.0.0-20260308193919-30a698fefa8b/go.mod h1:HQXAgmNSm7/FmBYwcsa6qpZqMrDhbLoEl+AyqFQ+RwY= github.com/TicketsBot-cloud/gdl v0.0.0-20260306134952-cccb0116fef6 h1:ucG0xLPt7xixW7/LvL0hXDBDouDRS1Nf+77qP8iJ/X0= From 7bad15e8ba596c516ed66d17a0af69315e469f63 Mon Sep 17 00:00:00 2001 From: biast12 Date: Sun, 12 Apr 2026 22:06:46 +0200 Subject: [PATCH 4/4] Refactor permission checks to botpermissions Replace manual permission aggregation with botpermissions.EffectivePermissions and consolidate data fetching into fetchGuildData/fetchChannelData/buildRoleMap helpers. Simplifies HasPermissions/HasPermissionsChannel and GetMissingPermissions* by delegating to centralized logic, removes many in-file permission helpers, and updates imports accordingly. Keeps existing error handling (sentry) and preserves administrator short-circuit behavior. --- bot/permissionwrapper/permission.go | 284 ++++------------------------ 1 file changed, 35 insertions(+), 249 deletions(-) diff --git a/bot/permissionwrapper/permission.go b/bot/permissionwrapper/permission.go index cf66095..4c96f9f 100644 --- a/bot/permissionwrapper/permission.go +++ b/bot/permissionwrapper/permission.go @@ -1,310 +1,96 @@ 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" ) 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 { 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 { - 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 -} - -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) - } - } - - 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) + m, roles, ch, err := fetchChannelData(ctx, guildId, userId, channelId) 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 -} - -func getEffectivePermissionsChannel(ctx *worker.Context, guildId, userId, channelId 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 - } - - 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 + return botpermissions.MissingPermissions(guildId, userId, []uint64(m.Roles), ch.PermissionOverwrites, buildRoleMap(roles), required) } -func getEffectivePermissions(ctx *worker.Context, guildId, userId 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 - } - - 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 + return member.Member{}, nil, 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 - } - } - } - } - } - - 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) +func fetchChannelData(ctx *worker.Context, guildId, userId, channelId uint64) (member.Member, []guild.Role, channel.Channel, error) { + m, roles, err := fetchGuildData(ctx, guildId, userId) if err != nil { - return 0, err + return member.Member{}, nil, channel.Channel{}, 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") - } - ch, err := ctx.GetChannel(channelId) if err != nil { - return 0, err - } - - for _, overwrite := range ch.PermissionOverwrites { - if overwrite.Type == channel.PermissionTypeRole && overwrite.Id == publicRole.Id { - initialPermissions &= ^overwrite.Deny - initialPermissions |= overwrite.Allow - break - } + return member.Member{}, nil, channel.Channel{}, err } - - return initialPermissions, nil + return m, roles, ch, 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 +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 } - - roles, err := ctx.GetGuildRoles(guildId) - if err != nil { - return 0, err - } - - for _, memberRole := range member.Roles { - for _, role := range roles { - if memberRole == role.Id { - initialPermissions |= role.Permissions - } - } - } - - return initialPermissions, 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 - } - } - - if publicRole == nil { - return 0, errors.New("couldn't find public role") - } - - return publicRole.Permissions, nil + return m }