From 2c9e9751693a7194c118d6700b2643ff445e7061 Mon Sep 17 00:00:00 2001 From: biast12 <53872542+biast12@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:36:24 +0200 Subject: [PATCH 1/2] Handle inaccessible channels in panels Exclude obfuscated channels from API results and reject inaccessible panel, category, transcript, and notification channels during validation. Expose no-access permission results in the frontend and update panel forms to validate channel requirements according to thread mode. --- backend/app/http/endpoints/api/channels.go | 4 ++ .../endpoints/api/panel/multipanelcreate.go | 12 +++- .../app/http/endpoints/api/panel/permcheck.go | 8 +++ .../http/endpoints/api/panel/validation.go | 61 +++++++++++++++++-- backend/go.mod | 2 + .../components/PermissionWarningBanner.tsx | 10 ++- .../src/pages/manage/multipanels/create.tsx | 3 +- .../src/pages/manage/multipanels/edit.tsx | 3 +- frontend/src/pages/manage/panels/create.tsx | 8 +-- frontend/src/pages/manage/panels/edit.tsx | 8 +-- frontend/src/types.d.ts | 2 + 11 files changed, 102 insertions(+), 19 deletions(-) diff --git a/backend/app/http/endpoints/api/channels.go b/backend/app/http/endpoints/api/channels.go index 7e45fd22..b15ecf8d 100644 --- a/backend/app/http/endpoints/api/channels.go +++ b/backend/app/http/endpoints/api/channels.go @@ -65,6 +65,10 @@ func ChannelsHandler(ctx *gin.Context) { continue } + if ch.IsObfuscated() { + continue + } + filtered = append(filtered, ch) } diff --git a/backend/app/http/endpoints/api/panel/multipanelcreate.go b/backend/app/http/endpoints/api/panel/multipanelcreate.go index d40169e4..b2d6599b 100644 --- a/backend/app/http/endpoints/api/panel/multipanelcreate.go +++ b/backend/app/http/endpoints/api/panel/multipanelcreate.go @@ -246,10 +246,16 @@ func (d *multiPanelCreateData) validateChannel(guildId uint64) func() error { var valid bool for _, ch := range channels { - if ch.Id == d.ChannelId && (ch.Type == channel.ChannelTypeGuildText || ch.Type == channel.ChannelTypeGuildNews) { - valid = true - break + if ch.Id != d.ChannelId { + continue } + + if ch.IsObfuscated() { + return errChannelNoAccess("The selected channel") + } + + valid = ch.Type == channel.ChannelTypeGuildText || ch.Type == channel.ChannelTypeGuildNews + break } if !valid { diff --git a/backend/app/http/endpoints/api/panel/permcheck.go b/backend/app/http/endpoints/api/panel/permcheck.go index 8c0977fb..e1a102d7 100644 --- a/backend/app/http/endpoints/api/panel/permcheck.go +++ b/backend/app/http/endpoints/api/panel/permcheck.go @@ -23,6 +23,7 @@ type channelCheckResult struct { Required []string `json:"required"` Missing []string `json:"missing"` Deleted bool `json:"deleted"` + NoAccess bool `json:"no_access"` Ok bool `json:"ok"` } @@ -204,6 +205,13 @@ func checkChannel( return result } + if ch.IsObfuscated() { + result.NoAccess = true + result.Missing = []string{permission.ViewChannel.String()} + result.Ok = false + return result + } + result.ChannelName = ch.Name missing := botpermissions.MissingPermissions(guildId, botId, botRoles, ch.PermissionOverwrites, roleMap, required) diff --git a/backend/app/http/endpoints/api/panel/validation.go b/backend/app/http/endpoints/api/panel/validation.go index aa842a6f..12ada6e1 100644 --- a/backend/app/http/endpoints/api/panel/validation.go +++ b/backend/app/http/endpoints/api/panel/validation.go @@ -114,12 +114,26 @@ func validateContent(ctx PanelValidationContext) validation.ValidationFunc { } } +func errChannelNoAccess(what string) error { + return validation.NewInvalidInputError(fmt.Sprintf("%s is not visible to the bot - grant it the View Channel permission", what)) +} + func validateChannelId(ctx PanelValidationContext) validation.ValidationFunc { return func() error { for _, ch := range ctx.Channels { - if ch.Id == ctx.Data.ChannelId && (ch.Type == channel.ChannelTypeGuildText || ch.Type == channel.ChannelTypeGuildNews) { + if ch.Id != ctx.Data.ChannelId { + continue + } + + if ch.IsObfuscated() { + return errChannelNoAccess("Panel channel") + } + + if ch.Type == channel.ChannelTypeGuildText || ch.Type == channel.ChannelTypeGuildNews { return nil } + + break } return validation.NewInvalidInputError("Panel channel not found") @@ -128,10 +142,24 @@ func validateChannelId(ctx PanelValidationContext) validation.ValidationFunc { func validateCategory(ctx PanelValidationContext) validation.ValidationFunc { return func() error { + if ctx.Data.UseThreads { + return nil + } + for _, ch := range ctx.Channels { - if ch.Id == ctx.Data.CategoryId && ch.Type == channel.ChannelTypeGuildCategory { + if ch.Id != ctx.Data.CategoryId { + continue + } + + if ch.IsObfuscated() { + return errChannelNoAccess("Ticket category") + } + + if ch.Type == channel.ChannelTypeGuildCategory { return nil } + + break } return validation.NewInvalidInputError("Invalid ticket category") @@ -279,9 +307,19 @@ func validatePendingCategory(ctx PanelValidationContext) validation.ValidationFu } for _, ch := range ctx.Channels { - if ch.Id == *ctx.Data.PendingCategory && ch.Type == channel.ChannelTypeGuildCategory { + if ch.Id != *ctx.Data.PendingCategory { + continue + } + + if ch.IsObfuscated() && !ctx.Data.UseThreads { + return errChannelNoAccess("Awaiting response category") + } + + if ch.Type == channel.ChannelTypeGuildCategory { return nil } + + break } return validation.NewInvalidInputError("Invalid awaiting response category") @@ -486,6 +524,9 @@ func validateTranscriptChannelId(ctx PanelValidationContext) validation.Validati for _, ch := range ctx.Channels { if ch.Id == *ctx.Data.TranscriptChannelId { + if ch.IsObfuscated() { + return errChannelNoAccess("Transcript channel") + } if ch.Type != channel.ChannelTypeGuildText && ch.Type != channel.ChannelTypeGuildNews { return validation.NewInvalidInputError("Transcript channel must be a text channel") } @@ -499,12 +540,18 @@ func validateTranscriptChannelId(ctx PanelValidationContext) validation.Validati func validateTicketNotificationChannel(ctx PanelValidationContext) validation.ValidationFunc { return func() error { - // Always validate the channel if provided + if !ctx.Data.UseThreads { + return nil + } + if ctx.Data.TicketNotificationChannel != nil { channelFound := false for _, ch := range ctx.Channels { if ch.Id == *ctx.Data.TicketNotificationChannel { channelFound = true + if ch.IsObfuscated() { + return errChannelNoAccess("Ticket notification channel") + } if ch.Type != channel.ChannelTypeGuildText { return validation.NewInvalidInputError("Ticket notification channel must be a text channel") } @@ -518,7 +565,7 @@ func validateTicketNotificationChannel(ctx PanelValidationContext) validation.Va } // Thread mode requires a per-panel notification channel - if ctx.Data.UseThreads && ctx.Data.TicketNotificationChannel == nil { + if ctx.Data.TicketNotificationChannel == nil { return validation.NewInvalidInputError("You must select a ticket notification channel for this panel when thread mode is enabled") } @@ -548,6 +595,10 @@ func validateOverflowCategoryId(ctx PanelValidationContext) validation.Validatio for _, ch := range ctx.Channels { if ch.Id == *ctx.Data.OverflowCategoryId { + if ch.IsObfuscated() && !ctx.Data.UseThreads { + return errChannelNoAccess("Overflow category") + } + if ch.GuildId != ctx.GuildId { return validation.NewInvalidInputError("Overflow category guild ID does not match") } diff --git a/backend/go.mod b/backend/go.mod index a82fb5d4..d6560ac1 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,6 +8,8 @@ go 1.26.0 // replace github.com/TicketsBot-cloud/worker => ../../worker +replace github.com/TicketsBot-cloud/gdl => ../../gdl + replace github.com/go-playground/validator/v10 => github.com/go-playground/validator/v10 v10.14.0 require ( diff --git a/frontend/src/components/PermissionWarningBanner.tsx b/frontend/src/components/PermissionWarningBanner.tsx index 9c172e00..c8deb9b6 100644 --- a/frontend/src/components/PermissionWarningBanner.tsx +++ b/frontend/src/components/PermissionWarningBanner.tsx @@ -11,7 +11,6 @@ const ROLE_LABELS: Record = { transcript_channel: "Transcript channel", }; -// Maps a role to the appropriate noun for "no longer exists" messages const ROLE_ENTITY: Record = { ticket_category: "category", }; @@ -63,6 +62,15 @@ const PanelDetail: FC<{ panel: PanelPermResult; isError: boolean; guildId: strin {" - "} {ROLE_ENTITY[ch.role] ?? "channel"} no longer exists + ) : ch.no_access ? ( + <> + {" "} + ({ch.channel_id}) + {" - "} + + bot cannot see this {ROLE_ENTITY[ch.role] ?? "channel"}, grant it View Channel + + ) : ( <> {" "} diff --git a/frontend/src/pages/manage/multipanels/create.tsx b/frontend/src/pages/manage/multipanels/create.tsx index e00caecf..4b1602af 100644 --- a/frontend/src/pages/manage/multipanels/create.tsx +++ b/frontend/src/pages/manage/multipanels/create.tsx @@ -178,7 +178,8 @@ const MultiPanelsPage: FC = () => { // Returns the message so the Save button can reuse the rules. const saveBlocker = () => { if (!multiPanel.channel_id) return "Select a panel channel before creating the multi-panel."; - if (staleChannel) return "The selected panel channel no longer exists."; + if (staleChannel) + return "The selected panel channel no longer exists or the bot can't view it."; if (multiPanel.panels.length < 2) return "Select at least two panels before creating the multi-panel."; if (multiPanel.panels.length > 15) return "Multi-panels cannot contain more than 15 panels."; diff --git a/frontend/src/pages/manage/multipanels/edit.tsx b/frontend/src/pages/manage/multipanels/edit.tsx index 80dd29e1..b2dcf804 100644 --- a/frontend/src/pages/manage/multipanels/edit.tsx +++ b/frontend/src/pages/manage/multipanels/edit.tsx @@ -182,7 +182,8 @@ const MultiPanelsPage: FC = () => { // Returns the message so the Save button can reuse the rules. const saveBlocker = () => { if (!multiPanel?.channel_id) return "Select a panel channel before saving the multi-panel."; - if (staleChannel) return "The selected panel channel no longer exists."; + if (staleChannel) + return "The selected panel channel no longer exists or the bot can't view it."; if (tooFewPanels) return "Select at least two panels before saving the multi-panel."; if (multiPanel.panels.length > 15) return "Multi-panels cannot contain more than 15 panels."; if (labellessPanelCount > 0) return "Every dropdown panel needs a label."; diff --git a/frontend/src/pages/manage/panels/create.tsx b/frontend/src/pages/manage/panels/create.tsx index 1abff323..9214d77c 100644 --- a/frontend/src/pages/manage/panels/create.tsx +++ b/frontend/src/pages/manage/panels/create.tsx @@ -281,9 +281,9 @@ const PanelsPage: FC = () => { missingThreadChannel || buttonIdentityMissing || stale(panel.channel_id) || - stale(panel.category_id) || + (!panel.use_threads && stale(panel.category_id)) || stale(panel.transcript_channel_id) || - stale(panel.ticket_notification_channel) || + (panel.use_threads && stale(panel.ticket_notification_channel)) || welcomeMessageEmpty || invalidWelcomeMessageUrls.length > 0; @@ -495,7 +495,7 @@ const PanelsPage: FC = () => { c.type == 4) @@ -804,7 +804,7 @@ const EditPanelsPage: FC = () => { label="Thread Notification Channel" info={THREAD_NOTIFICATION_CHANNEL_INFO} required={panel.use_threads} - error={stale(panel.ticket_notification_channel)} + error={panel.use_threads && stale(panel.ticket_notification_channel)} disabled={!panel.use_threads} options={ selectedGuild?.channels diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index 3d02e478..b5595cc2 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -93,6 +93,7 @@ export interface GuildChannel { join_timestamp: string; // ISO 8601 format flags: number; // Flags for the member }; + flags?: number; // Additional fields can be added as needed } @@ -978,6 +979,7 @@ export interface ChannelPermCheckResult { required: string[]; missing: string[]; deleted: boolean; + no_access: boolean; ok: boolean; } From c5eb88b0fbce031c61cc9c6b3018398fe49827fe Mon Sep 17 00:00:00 2001 From: biast12 <53872542+biast12@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:50:14 +0200 Subject: [PATCH 2/2] Bump gdl --- backend/go.mod | 4 ++-- backend/go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index d6560ac1..6a5ad026 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,7 +8,7 @@ go 1.26.0 // replace github.com/TicketsBot-cloud/worker => ../../worker -replace github.com/TicketsBot-cloud/gdl => ../../gdl +// replace github.com/TicketsBot-cloud/gdl => ../../gdl replace github.com/go-playground/validator/v10 => github.com/go-playground/validator/v10 v10.14.0 @@ -17,7 +17,7 @@ require ( github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704 github.com/TicketsBot-cloud/common v0.0.0-20260905165836-38e4090764a4 github.com/TicketsBot-cloud/database v0.0.0-20260913165941-c2ecc7191b02 - github.com/TicketsBot-cloud/gdl v0.0.0-20260612070331-a3947b410d3e + github.com/TicketsBot-cloud/gdl v0.0.0-20260927072153-8dbf0b284f57 github.com/TicketsBot-cloud/logarchiver v0.0.0-20251018211319-7a7df5cacbdc github.com/TicketsBot-cloud/worker v0.0.0-20260827073646-455b39e53841 github.com/apex/log v1.1.2 diff --git a/backend/go.sum b/backend/go.sum index bf472c0e..42efd5e3 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -43,8 +43,8 @@ github.com/TicketsBot-cloud/common v0.0.0-20260905165836-38e4090764a4 h1:zcPcJ+0 github.com/TicketsBot-cloud/common v0.0.0-20260905165836-38e4090764a4/go.mod h1:yL+VPSYNVK5gxUkA+fbb0WdTjGfWXnr7ScfL0rwk29g= github.com/TicketsBot-cloud/database v0.0.0-20260913165941-c2ecc7191b02 h1:3Z8PpXBUbdYn2srMRg3Nj0MDxnTB53CzIEAOUBIubhM= github.com/TicketsBot-cloud/database v0.0.0-20260913165941-c2ecc7191b02/go.mod h1:0fKn7nPX6KeljYNQtGQiuZ5aJ/TczgXvBhnbz+4GEc8= -github.com/TicketsBot-cloud/gdl v0.0.0-20260612070331-a3947b410d3e h1:qCibZQmO2rrBFrvn6+oAchPEQ7k/0Izr3ruL5P9aVZU= -github.com/TicketsBot-cloud/gdl v0.0.0-20260612070331-a3947b410d3e/go.mod h1:CdwBR2egPtxUXjD2CgC9ZwfuB8dz9HPePM8nuG6dt7Y= +github.com/TicketsBot-cloud/gdl v0.0.0-20260927072153-8dbf0b284f57 h1:cFlQplQOlJCb+JQfxny+MJTg4dMKyEAvzB50MrWRa1E= +github.com/TicketsBot-cloud/gdl v0.0.0-20260927072153-8dbf0b284f57/go.mod h1:CdwBR2egPtxUXjD2CgC9ZwfuB8dz9HPePM8nuG6dt7Y= github.com/TicketsBot-cloud/logarchiver v0.0.0-20251018211319-7a7df5cacbdc h1:qTLNpCvIqM7UwZ6MdWQ9EztcDsIJfHh+VJdG+ULLEaA= github.com/TicketsBot-cloud/logarchiver v0.0.0-20251018211319-7a7df5cacbdc/go.mod h1:pZqkzPNNTqnwKZvCT8kCaTHxrG7HJbxZV83S0p7mmzM= github.com/TicketsBot-cloud/worker v0.0.0-20260827073646-455b39e53841 h1:TBabCBrj8XiiDey2iqicOA6HfDIyqZ63akGhImheoxs=