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 6220acbd..147c456c 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,14 +8,16 @@ 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 ( github.com/BurntSushi/toml v1.2.1 github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704 github.com/TicketsBot-cloud/common v0.0.0-20260927072029-424264fc4be2 - github.com/TicketsBot-cloud/database v0.0.0-20261002063446-01dca52a987e - github.com/TicketsBot-cloud/gdl v0.0.0-20260612070331-a3947b410d3e + github.com/TicketsBot-cloud/database v0.0.0-20260927071905-7a22c7be0e4a + 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 cb917e0d..6b745855 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -41,10 +41,10 @@ github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704 h1 github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704/go.mod h1:Mux1bEPpOHwRw1wo6Fa6qJLJH9Erk9qv1yAIfLi1Wmw= github.com/TicketsBot-cloud/common v0.0.0-20260927072029-424264fc4be2 h1:qeebeGGdrsDXrngOVrKMDt9G3JUTJwT/IEPTB61d46s= github.com/TicketsBot-cloud/common v0.0.0-20260927072029-424264fc4be2/go.mod h1:yL+VPSYNVK5gxUkA+fbb0WdTjGfWXnr7ScfL0rwk29g= -github.com/TicketsBot-cloud/database v0.0.0-20261002063446-01dca52a987e h1:kZS42Mp+6wBhApK7zA6F1SX8ZYYG18cVRHB2ZDTvx94= -github.com/TicketsBot-cloud/database v0.0.0-20261002063446-01dca52a987e/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/database v0.0.0-20260927071905-7a22c7be0e4a h1:Y1MPuFHwMmkoGeqnUV8CuXx4yqChdvPpqy3vGWBYnZo= +github.com/TicketsBot-cloud/database v0.0.0-20260927071905-7a22c7be0e4a/go.mod h1:0fKn7nPX6KeljYNQtGQiuZ5aJ/TczgXvBhnbz+4GEc8= +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= 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 4ce1b9dc..13499bca 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 } @@ -994,6 +995,7 @@ export interface ChannelPermCheckResult { required: string[]; missing: string[]; deleted: boolean; + no_access: boolean; ok: boolean; }