From 34051a9657f32278010b14615baaf959420b5c45 Mon Sep 17 00:00:00 2001 From: Ben Hall Date: Sun, 6 Sep 2026 17:47:53 +0100 Subject: [PATCH 1/4] feat: message performance Signed-off-by: Ben Hall --- multipanels.go | 44 +++++++++++----- multipaneltargets.go | 5 ++ panels.go | 119 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 145 insertions(+), 23 deletions(-) diff --git a/multipanels.go b/multipanels.go index 4a93eb2..586b5bf 100644 --- a/multipanels.go +++ b/multipanels.go @@ -15,6 +15,10 @@ type MultiPanel struct { SelectMenu bool `json:"select_menu"` SelectMenuPlaceholder *string `json:"select_menu_placeholder"` Embed *CustomEmbedWithFields `json:"embed"` + UsesComponentsV2 bool `json:"uses_components_v2"` + Components *string `json:"components"` + ForceDisabled bool `json:"force_disabled"` + Name *string `json:"name"` } type MultiPanelTable struct { @@ -40,13 +44,17 @@ CREATE TABLE IF NOT EXISTS multi_panels( PRIMARY KEY("id") ); CREATE INDEX IF NOT EXISTS multi_panels_guild_id ON multi_panels("guild_id"); -CREATE INDEX IF NOT EXISTS multi_panels_message_id ON multi_panels("message_id");` +CREATE INDEX IF NOT EXISTS multi_panels_message_id ON multi_panels("message_id"); +ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "uses_components_v2" bool NOT NULL DEFAULT false; +ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "components" JSONB DEFAULT NULL; +ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "force_disabled" bool NOT NULL DEFAULT false; +ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "name" text DEFAULT NULL;` } func (p *MultiPanelTable) Get(ctx context.Context, id int) (MultiPanel, bool, error) { query := ` SELECT - "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed" + "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "force_disabled", "name" FROM multi_panels WHERE @@ -56,7 +64,7 @@ WHERE var panel MultiPanel var embedRaw *string err := p.QueryRow(ctx, query, id).Scan( - &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, + &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.ForceDisabled, &panel.Name, ) if err != nil { @@ -79,7 +87,7 @@ WHERE func (p *MultiPanelTable) GetByMessageId(ctx context.Context, messageId uint64) (MultiPanel, bool, error) { query := ` SELECT - "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed" + "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "force_disabled", "name" FROM multi_panels WHERE @@ -89,7 +97,7 @@ WHERE var panel MultiPanel var embedRaw *string err := p.QueryRow(ctx, query, messageId).Scan( - &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, + &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.ForceDisabled, &panel.Name, ) if err != nil { @@ -111,7 +119,7 @@ WHERE func (p *MultiPanelTable) GetByGuild(ctx context.Context, guildId uint64) ([]MultiPanel, error) { query := ` -SELECT "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed" +SELECT "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "force_disabled", "name" FROM multi_panels WHERE "guild_id" = $1; ` @@ -127,7 +135,7 @@ WHERE "guild_id" = $1; var panel MultiPanel var embedRaw *string err := rows.Scan( - &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, + &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.ForceDisabled, &panel.Name, ) if err != nil { @@ -149,9 +157,9 @@ WHERE "guild_id" = $1; func (p *MultiPanelTable) Create(ctx context.Context, panel MultiPanel) (int, error) { query := ` INSERT INTO - multi_panels("message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed") + multi_panels("message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "force_disabled", "name") VALUES - ($1, $2, $3, $4, $5, $6) + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING "id" ; @@ -169,7 +177,7 @@ RETURNING var multiPanelId int if err := p.QueryRow(ctx, query, - panel.MessageId, panel.ChannelId, panel.GuildId, panel.SelectMenu, panel.SelectMenuPlaceholder, embedRaw, + panel.MessageId, panel.ChannelId, panel.GuildId, panel.SelectMenu, panel.SelectMenuPlaceholder, embedRaw, panel.UsesComponentsV2, panel.Components, panel.ForceDisabled, panel.Name, ).Scan(&multiPanelId); err != nil { return 0, err } @@ -184,7 +192,11 @@ UPDATE multi_panels "channel_id" = $3, "select_menu" = $4, "select_menu_placeholder" = $5, - "embed" = $6 + "embed" = $6, + "uses_components_v2" = $7, + "components" = $8, + "force_disabled" = $9, + "name" = $10 WHERE "id" = $1 ;` @@ -200,7 +212,7 @@ UPDATE multi_panels } _, err = p.Exec(ctx, query, - multiPanelId, multiPanel.MessageId, multiPanel.ChannelId, multiPanel.SelectMenu, multiPanel.SelectMenuPlaceholder, embedRaw, + multiPanelId, multiPanel.MessageId, multiPanel.ChannelId, multiPanel.SelectMenu, multiPanel.SelectMenuPlaceholder, embedRaw, multiPanel.UsesComponentsV2, multiPanel.Components, multiPanel.ForceDisabled, multiPanel.Name, ) return @@ -217,6 +229,14 @@ WHERE "id" = $2; return } +// SetForceDisabled sets force_disabled for all of a guild's multi-panels that use Components V2. +// Used by the premium downgrade sweep. +func (p *MultiPanelTable) SetForceDisabled(ctx context.Context, guildId uint64, forceDisabled bool) error { + query := `UPDATE multi_panels SET "force_disabled" = $2 WHERE "guild_id" = $1 AND "uses_components_v2" = true;` + _, err := p.Exec(ctx, query, guildId, forceDisabled) + return err +} + func (p *MultiPanelTable) Delete(ctx context.Context, guildId uint64, multiPanelId int) (success bool, err error) { query := ` WITH deleted AS ( diff --git a/multipaneltargets.go b/multipaneltargets.go index 572ae44..9ffc1a4 100644 --- a/multipaneltargets.go +++ b/multipaneltargets.go @@ -88,6 +88,11 @@ SELECT panels.feedback_enabled, panels.support_can_view, panels.support_can_type, + panels.message_uses_components_v2, + panels.message_components, + panels.welcome_message_uses_components_v2, + panels.welcome_message_components, + panels.name, multi_panel_targets.custom_label, multi_panel_targets.description, multi_panel_targets.custom_emoji_name, diff --git a/panels.go b/panels.go index c87458a..057deb6 100644 --- a/panels.go +++ b/panels.go @@ -50,6 +50,11 @@ type Panel struct { FeedbackEnabled bool `json:"feedback_enabled"` SupportCanView bool `json:"support_can_view"` SupportCanType bool `json:"support_can_type"` + MessageUsesComponentsV2 bool `json:"message_uses_components_v2"` + MessageComponents *string `json:"message_components"` + WelcomeMessageUsesComponentsV2 bool `json:"welcome_message_uses_components_v2"` + WelcomeMessageComponents *string `json:"welcome_message_components"` + Name *string `json:"name"` } type PanelWithWelcomeMessage struct { @@ -122,7 +127,12 @@ CREATE INDEX IF NOT EXISTS panels_guild_id ON panels("guild_id"); CREATE INDEX IF NOT EXISTS panels_message_id ON panels("message_id"); CREATE INDEX IF NOT EXISTS panels_form_id ON panels("form_id"); CREATE INDEX IF NOT EXISTS panels_guild_id_form_id ON panels("guild_id", "form_id"); -CREATE INDEX IF NOT EXISTS panels_custom_id ON panels("custom_id");` +CREATE INDEX IF NOT EXISTS panels_custom_id ON panels("custom_id"); +ALTER TABLE panels ADD COLUMN IF NOT EXISTS "message_uses_components_v2" bool NOT NULL DEFAULT false; +ALTER TABLE panels ADD COLUMN IF NOT EXISTS "message_components" JSONB DEFAULT NULL; +ALTER TABLE panels ADD COLUMN IF NOT EXISTS "welcome_message_uses_components_v2" bool NOT NULL DEFAULT false; +ALTER TABLE panels ADD COLUMN IF NOT EXISTS "welcome_message_components" JSONB DEFAULT NULL; +ALTER TABLE panels ADD COLUMN IF NOT EXISTS "name" text DEFAULT NULL;` } func (p *PanelTable) Get(ctx context.Context, messageId uint64) (panel Panel, e error) { @@ -169,7 +179,12 @@ SELECT close_confirmation, feedback_enabled, support_can_view, - support_can_type + support_can_type, + message_uses_components_v2, + message_components, + welcome_message_uses_components_v2, + welcome_message_components, + name FROM panels WHERE "message_id" = $1; ` @@ -226,7 +241,12 @@ SELECT close_confirmation, feedback_enabled, support_can_view, - support_can_type + support_can_type, + message_uses_components_v2, + message_components, + welcome_message_uses_components_v2, + welcome_message_components, + name FROM panels WHERE "panel_id" = $1; ` @@ -284,6 +304,11 @@ SELECT panels.feedback_enabled, panels.support_can_view, panels.support_can_type, + panels.message_uses_components_v2, + panels.message_components, + panels.welcome_message_uses_components_v2, + panels.welcome_message_components, + panels.name, embeds.id, embeds.guild_id, embeds.title, @@ -402,7 +427,12 @@ SELECT close_confirmation, feedback_enabled, support_can_view, - support_can_type + support_can_type, + message_uses_components_v2, + message_components, + welcome_message_uses_components_v2, + welcome_message_components, + name FROM panels WHERE "guild_id" = $1 AND "custom_id" = $2; ` @@ -462,7 +492,12 @@ SELECT close_confirmation, feedback_enabled, support_can_view, - support_can_type + support_can_type, + message_uses_components_v2, + message_components, + welcome_message_uses_components_v2, + welcome_message_components, + name FROM panels WHERE "guild_id" = $1 AND "form_id" = $2; ` @@ -522,7 +557,12 @@ SELECT panels.close_confirmation, panels.feedback_enabled, panels.support_can_view, - panels.support_can_type + panels.support_can_type, + panels.message_uses_components_v2, + panels.message_components, + panels.welcome_message_uses_components_v2, + panels.welcome_message_components, + panels.name FROM panels INNER JOIN forms ON forms.form_id = panels.form_id @@ -584,7 +624,12 @@ SELECT close_confirmation, feedback_enabled, support_can_view, - support_can_type + support_can_type, + message_uses_components_v2, + message_components, + welcome_message_uses_components_v2, + welcome_message_components, + name FROM panels WHERE "guild_id" = $1 ORDER BY "panel_id" ASC;` @@ -652,6 +697,11 @@ SELECT panels.feedback_enabled, panels.support_can_view, panels.support_can_type, + panels.message_uses_components_v2, + panels.message_components, + panels.welcome_message_uses_components_v2, + panels.welcome_message_components, + panels.name, embeds.id, embeds.guild_id, embeds.title, @@ -787,9 +837,14 @@ INSERT INTO panels( "close_confirmation", "feedback_enabled", "support_can_view", - "support_can_type" + "support_can_type", + "message_uses_components_v2", + "message_components", + "welcome_message_uses_components_v2", + "welcome_message_components", + "name" ) -VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41) +VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46) ON CONFLICT("message_id") DO NOTHING RETURNING "panel_id";` @@ -835,6 +890,11 @@ RETURNING "panel_id";` panel.FeedbackEnabled, panel.SupportCanView, panel.SupportCanType, + panel.MessageUsesComponentsV2, + panel.MessageComponents, + panel.WelcomeMessageUsesComponentsV2, + panel.WelcomeMessageComponents, + panel.Name, ).Scan(&panelId) return @@ -897,7 +957,12 @@ UPDATE panels "close_confirmation" = $38, "feedback_enabled" = $39, "support_can_view" = $40, - "support_can_type" = $41 + "support_can_type" = $41, + "message_uses_components_v2" = $42, + "message_components" = $43, + "welcome_message_uses_components_v2" = $44, + "welcome_message_components" = $45, + "name" = $46 WHERE "panel_id" = $1 ;` @@ -944,6 +1009,11 @@ UPDATE panels panel.FeedbackEnabled, panel.SupportCanView, panel.SupportCanType, + panel.MessageUsesComponentsV2, + panel.MessageComponents, + panel.WelcomeMessageUsesComponentsV2, + panel.WelcomeMessageComponents, + panel.Name, ) return err @@ -1010,12 +1080,16 @@ AND "panel_id" IN ( } } else if panelCount < freeLimit { // Too few enabled: re-enable force-disabled panels to fill the vacancy, oldest first. + // Never re-enable a panel force-disabled for using Components V2 - that disablement + // is a premium gate, not part of the free panel quota this function balances, and + // re-enabling it here would silently restore a premium feature to a free-tier guild. query := ` UPDATE panels SET "force_disabled" = false WHERE "guild_id" = $1 +AND "message_uses_components_v2" = false AND "panel_id" IN ( SELECT "panel_id" FROM panels - WHERE "guild_id" = $1 AND "force_disabled" = true + WHERE "guild_id" = $1 AND "force_disabled" = true AND "message_uses_components_v2" = false ORDER BY "panel_id" ASC LIMIT $2 );` if _, err := tx.Exec(ctx, query, guildId, freeLimit-panelCount); err != nil { @@ -1026,6 +1100,24 @@ AND "panel_id" IN ( return tx.Commit(ctx) } +// SetComponentsV2ForceDisabled sets force_disabled for all of a guild's panels that use +// a Components V2 button message. Used by the premium downgrade sweep. +func (p *PanelTable) SetComponentsV2ForceDisabled(ctx context.Context, guildId uint64, forceDisabled bool) error { + query := `UPDATE panels SET "force_disabled" = $2 WHERE "guild_id" = $1 AND "message_uses_components_v2" = true;` + _, err := p.Exec(ctx, query, guildId, forceDisabled) + return err +} + +// ClearWelcomeMessageComponentsV2 disables Components V2 for a guild's welcome messages and +// clears the stored component tree, without touching the panel's button message or force_disabled +// state. Used by the premium downgrade sweep: losing Components V2 access on the welcome message +// falls back to the classic hardcoded welcome text, it does not disable the panel. +func (p *PanelTable) ClearWelcomeMessageComponentsV2(ctx context.Context, guildId uint64) error { + query := `UPDATE panels SET "welcome_message_uses_components_v2" = false, "welcome_message_components" = NULL WHERE "guild_id" = $1 AND "welcome_message_uses_components_v2" = true;` + _, err := p.Exec(ctx, query, guildId) + return err +} + func (p *PanelTable) Delete(ctx context.Context, panelId int) (err error) { query := `DELETE FROM panels WHERE "panel_id"=$1;` _, err = p.Exec(ctx, query, panelId) @@ -1076,5 +1168,10 @@ func (p *Panel) fieldPtrs() []interface{} { &p.FeedbackEnabled, &p.SupportCanView, &p.SupportCanType, + &p.MessageUsesComponentsV2, + &p.MessageComponents, + &p.WelcomeMessageUsesComponentsV2, + &p.WelcomeMessageComponents, + &p.Name, } } From c1e7e5a0127e4dbb202743901d404bf8e961c5d8 Mon Sep 17 00:00:00 2001 From: Ben Hall Date: Wed, 14 Oct 2026 17:06:16 +0100 Subject: [PATCH 2/4] fixes based on feedback Signed-off-by: Ben Hall --- multipanels.go | 35 ++++++---------- multipaneltargets.go | 11 ++++- panels.go | 98 +++++++++++++++++++++++++++++++++----------- 3 files changed, 97 insertions(+), 47 deletions(-) diff --git a/multipanels.go b/multipanels.go index 586b5bf..4e743ab 100644 --- a/multipanels.go +++ b/multipanels.go @@ -17,7 +17,6 @@ type MultiPanel struct { Embed *CustomEmbedWithFields `json:"embed"` UsesComponentsV2 bool `json:"uses_components_v2"` Components *string `json:"components"` - ForceDisabled bool `json:"force_disabled"` Name *string `json:"name"` } @@ -54,7 +53,7 @@ ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "name" text DEFAULT NULL;` func (p *MultiPanelTable) Get(ctx context.Context, id int) (MultiPanel, bool, error) { query := ` SELECT - "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "force_disabled", "name" + "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "name" FROM multi_panels WHERE @@ -64,7 +63,7 @@ WHERE var panel MultiPanel var embedRaw *string err := p.QueryRow(ctx, query, id).Scan( - &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.ForceDisabled, &panel.Name, + &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.Name, ) if err != nil { @@ -87,7 +86,7 @@ WHERE func (p *MultiPanelTable) GetByMessageId(ctx context.Context, messageId uint64) (MultiPanel, bool, error) { query := ` SELECT - "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "force_disabled", "name" + "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "name" FROM multi_panels WHERE @@ -97,7 +96,7 @@ WHERE var panel MultiPanel var embedRaw *string err := p.QueryRow(ctx, query, messageId).Scan( - &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.ForceDisabled, &panel.Name, + &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.Name, ) if err != nil { @@ -119,9 +118,10 @@ WHERE func (p *MultiPanelTable) GetByGuild(ctx context.Context, guildId uint64) ([]MultiPanel, error) { query := ` -SELECT "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "force_disabled", "name" +SELECT "id", "message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "name" FROM multi_panels -WHERE "guild_id" = $1; +WHERE "guild_id" = $1 +ORDER BY "id" ASC; ` rows, err := p.Query(ctx, query, guildId) @@ -135,7 +135,7 @@ WHERE "guild_id" = $1; var panel MultiPanel var embedRaw *string err := rows.Scan( - &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.ForceDisabled, &panel.Name, + &panel.Id, &panel.MessageId, &panel.ChannelId, &panel.GuildId, &panel.SelectMenu, &panel.SelectMenuPlaceholder, &embedRaw, &panel.UsesComponentsV2, &panel.Components, &panel.Name, ) if err != nil { @@ -157,9 +157,9 @@ WHERE "guild_id" = $1; func (p *MultiPanelTable) Create(ctx context.Context, panel MultiPanel) (int, error) { query := ` INSERT INTO - multi_panels("message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "force_disabled", "name") + multi_panels("message_id", "channel_id", "guild_id", "select_menu", "select_menu_placeholder", "embed", "uses_components_v2", "components", "name") VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING "id" ; @@ -177,7 +177,7 @@ RETURNING var multiPanelId int if err := p.QueryRow(ctx, query, - panel.MessageId, panel.ChannelId, panel.GuildId, panel.SelectMenu, panel.SelectMenuPlaceholder, embedRaw, panel.UsesComponentsV2, panel.Components, panel.ForceDisabled, panel.Name, + panel.MessageId, panel.ChannelId, panel.GuildId, panel.SelectMenu, panel.SelectMenuPlaceholder, embedRaw, panel.UsesComponentsV2, panel.Components, panel.Name, ).Scan(&multiPanelId); err != nil { return 0, err } @@ -195,8 +195,7 @@ UPDATE multi_panels "embed" = $6, "uses_components_v2" = $7, "components" = $8, - "force_disabled" = $9, - "name" = $10 + "name" = $9 WHERE "id" = $1 ;` @@ -212,7 +211,7 @@ UPDATE multi_panels } _, err = p.Exec(ctx, query, - multiPanelId, multiPanel.MessageId, multiPanel.ChannelId, multiPanel.SelectMenu, multiPanel.SelectMenuPlaceholder, embedRaw, multiPanel.UsesComponentsV2, multiPanel.Components, multiPanel.ForceDisabled, multiPanel.Name, + multiPanelId, multiPanel.MessageId, multiPanel.ChannelId, multiPanel.SelectMenu, multiPanel.SelectMenuPlaceholder, embedRaw, multiPanel.UsesComponentsV2, multiPanel.Components, multiPanel.Name, ) return @@ -229,14 +228,6 @@ WHERE "id" = $2; return } -// SetForceDisabled sets force_disabled for all of a guild's multi-panels that use Components V2. -// Used by the premium downgrade sweep. -func (p *MultiPanelTable) SetForceDisabled(ctx context.Context, guildId uint64, forceDisabled bool) error { - query := `UPDATE multi_panels SET "force_disabled" = $2 WHERE "guild_id" = $1 AND "uses_components_v2" = true;` - _, err := p.Exec(ctx, query, guildId, forceDisabled) - return err -} - func (p *MultiPanelTable) Delete(ctx context.Context, guildId uint64, multiPanelId int) (success bool, err error) { query := ` WITH deleted AS ( diff --git a/multipaneltargets.go b/multipaneltargets.go index 9ffc1a4..bc97541 100644 --- a/multipaneltargets.go +++ b/multipaneltargets.go @@ -141,11 +141,15 @@ SELECT multi_panels.guild_id, multi_panels.select_menu, multi_panels.select_menu_placeholder, - multi_panels.embed + multi_panels.embed, + multi_panels.uses_components_v2, + multi_panels.components, + multi_panels.name FROM multi_panel_targets INNER JOIN multi_panels ON multi_panels.id = multi_panel_targets.multi_panel_id -WHERE multi_panel_targets.panel_id = $1; +WHERE multi_panel_targets.panel_id = $1 +ORDER BY multi_panels.id ASC; ` rows, err := p.Query(ctx, query, panelId) @@ -166,6 +170,9 @@ WHERE multi_panel_targets.panel_id = $1; &multiPanel.SelectMenu, &multiPanel.SelectMenuPlaceholder, &embedRaw, + &multiPanel.UsesComponentsV2, + &multiPanel.Components, + &multiPanel.Name, ) if err != nil { diff --git a/panels.go b/panels.go index 057deb6..97e4dec 100644 --- a/panels.go +++ b/panels.go @@ -652,6 +652,80 @@ ORDER BY "panel_id" ASC;` return } +// GetByGuildWithoutComponents is GetByGuild without loading the component trees, which are +// left nil. For hot paths that never read them. +func (p *PanelTable) GetByGuildWithoutComponents(ctx context.Context, guildId uint64) (panels []Panel, e error) { + query := ` +SELECT + panel_id, + message_id, + channel_id, + guild_id, + title, + content, + colour, + target_category, + emoji_name, + emoji_id, + emoji_animated, + welcome_message, + default_team, + custom_id, + image_url, + thumbnail_url, + button_style, + button_label, + form_id, + naming_scheme, + force_disabled, + disabled, + exit_survey_form_id, + pending_category, + mention_behaviour, + transcript_channel_id, + use_threads, + ticket_notification_channel, + cooldown_seconds, + ticket_limit, + hide_close_button, + hide_close_with_reason_button, + hide_claim_button, + show_in_open_command, + store_transcripts, + overflow_enabled, + overflow_category_id, + users_can_close, + close_confirmation, + feedback_enabled, + support_can_view, + support_can_type, + message_uses_components_v2, + NULL::text, + welcome_message_uses_components_v2, + NULL::text, + name +FROM panels +WHERE "guild_id" = $1 +ORDER BY "panel_id" ASC;` + + rows, err := p.Query(ctx, query, guildId) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var panel Panel + if err := rows.Scan(panel.fieldPtrs()...); err != nil { + return nil, err + } + + panels = append(panels, panel) + } + + return +} + func (p *PanelTable) GetByGuildWithWelcomeMessage(ctx context.Context, guildId uint64) (panels []PanelWithWelcomeMessage, e error) { query := ` SELECT @@ -1080,16 +1154,12 @@ AND "panel_id" IN ( } } else if panelCount < freeLimit { // Too few enabled: re-enable force-disabled panels to fill the vacancy, oldest first. - // Never re-enable a panel force-disabled for using Components V2 - that disablement - // is a premium gate, not part of the free panel quota this function balances, and - // re-enabling it here would silently restore a premium feature to a free-tier guild. query := ` UPDATE panels SET "force_disabled" = false WHERE "guild_id" = $1 -AND "message_uses_components_v2" = false AND "panel_id" IN ( SELECT "panel_id" FROM panels - WHERE "guild_id" = $1 AND "force_disabled" = true AND "message_uses_components_v2" = false + WHERE "guild_id" = $1 AND "force_disabled" = true ORDER BY "panel_id" ASC LIMIT $2 );` if _, err := tx.Exec(ctx, query, guildId, freeLimit-panelCount); err != nil { @@ -1100,24 +1170,6 @@ AND "panel_id" IN ( return tx.Commit(ctx) } -// SetComponentsV2ForceDisabled sets force_disabled for all of a guild's panels that use -// a Components V2 button message. Used by the premium downgrade sweep. -func (p *PanelTable) SetComponentsV2ForceDisabled(ctx context.Context, guildId uint64, forceDisabled bool) error { - query := `UPDATE panels SET "force_disabled" = $2 WHERE "guild_id" = $1 AND "message_uses_components_v2" = true;` - _, err := p.Exec(ctx, query, guildId, forceDisabled) - return err -} - -// ClearWelcomeMessageComponentsV2 disables Components V2 for a guild's welcome messages and -// clears the stored component tree, without touching the panel's button message or force_disabled -// state. Used by the premium downgrade sweep: losing Components V2 access on the welcome message -// falls back to the classic hardcoded welcome text, it does not disable the panel. -func (p *PanelTable) ClearWelcomeMessageComponentsV2(ctx context.Context, guildId uint64) error { - query := `UPDATE panels SET "welcome_message_uses_components_v2" = false, "welcome_message_components" = NULL WHERE "guild_id" = $1 AND "welcome_message_uses_components_v2" = true;` - _, err := p.Exec(ctx, query, guildId) - return err -} - func (p *PanelTable) Delete(ctx context.Context, panelId int) (err error) { query := `DELETE FROM panels WHERE "panel_id"=$1;` _, err = p.Exec(ctx, query, panelId) From 2fbd7bc09ba2cdac3ed5dfe17762d8ddd6243778 Mon Sep 17 00:00:00 2001 From: Ben Hall Date: Sun, 18 Oct 2026 20:03:15 +0100 Subject: [PATCH 3/4] fixes based on feedback Signed-off-by: Ben Hall --- multipanels.go | 1 - 1 file changed, 1 deletion(-) diff --git a/multipanels.go b/multipanels.go index 4e743ab..950bba5 100644 --- a/multipanels.go +++ b/multipanels.go @@ -46,7 +46,6 @@ CREATE INDEX IF NOT EXISTS multi_panels_guild_id ON multi_panels("guild_id"); CREATE INDEX IF NOT EXISTS multi_panels_message_id ON multi_panels("message_id"); ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "uses_components_v2" bool NOT NULL DEFAULT false; ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "components" JSONB DEFAULT NULL; -ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "force_disabled" bool NOT NULL DEFAULT false; ALTER TABLE multi_panels ADD COLUMN IF NOT EXISTS "name" text DEFAULT NULL;` } From 03a277e94ec08ee57a815939cacb377cdd2bb929 Mon Sep 17 00:00:00 2001 From: biast12 <53872542+biast12@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:59:20 +0100 Subject: [PATCH 4/4] Preserve panel metadata during updates Prevent general panel and multipanel updates from clearing component and name fields by moving those changes into dedicated methods. Ticket counts now prefer configured panel names over titles. --- multipanels.go | 20 +++++++++++++++----- panels.go | 28 +++++++++++++++------------- tickets.go | 4 ++-- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/multipanels.go b/multipanels.go index 950bba5..93c5bd7 100644 --- a/multipanels.go +++ b/multipanels.go @@ -191,10 +191,7 @@ UPDATE multi_panels "channel_id" = $3, "select_menu" = $4, "select_menu_placeholder" = $5, - "embed" = $6, - "uses_components_v2" = $7, - "components" = $8, - "name" = $9 + "embed" = $6 WHERE "id" = $1 ;` @@ -210,12 +207,25 @@ UPDATE multi_panels } _, err = p.Exec(ctx, query, - multiPanelId, multiPanel.MessageId, multiPanel.ChannelId, multiPanel.SelectMenu, multiPanel.SelectMenuPlaceholder, embedRaw, multiPanel.UsesComponentsV2, multiPanel.Components, multiPanel.Name, + multiPanelId, multiPanel.MessageId, multiPanel.ChannelId, multiPanel.SelectMenu, multiPanel.SelectMenuPlaceholder, embedRaw, ) return } +// Kept out of Update so callers that don't set these fields can't clear them. +func (p *MultiPanelTable) UpdateComponentsV2(ctx context.Context, multiPanelId int, usesComponentsV2 bool, components *string) error { + query := `UPDATE multi_panels SET "uses_components_v2" = $2, "components" = $3 WHERE "id" = $1;` + _, err := p.Exec(ctx, query, multiPanelId, usesComponentsV2, components) + return err +} + +func (p *MultiPanelTable) SetName(ctx context.Context, multiPanelId int, name *string) error { + query := `UPDATE multi_panels SET "name" = $2 WHERE "id" = $1;` + _, err := p.Exec(ctx, query, multiPanelId, name) + return err +} + func (p *MultiPanelTable) UpdateMessageId(ctx context.Context, multiPanelId int, messageId uint64) (err error) { query := ` UPDATE multi_panels diff --git a/panels.go b/panels.go index fab40f2..e08f355 100644 --- a/panels.go +++ b/panels.go @@ -653,8 +653,7 @@ ORDER BY "panel_id" ASC;` return } -// GetByGuildWithoutComponents is GetByGuild without loading the component trees, which are -// left nil. For hot paths that never read them. +// For hot paths that never read the component trees, which are left nil. func (p *PanelTable) GetByGuildWithoutComponents(ctx context.Context, guildId uint64) (panels []Panel, e error) { query := ` SELECT @@ -1032,12 +1031,7 @@ UPDATE panels "close_confirmation" = $38, "feedback_enabled" = $39, "support_can_view" = $40, - "support_can_type" = $41, - "message_uses_components_v2" = $42, - "message_components" = $43, - "welcome_message_uses_components_v2" = $44, - "welcome_message_components" = $45, - "name" = $46 + "support_can_type" = $41 WHERE "panel_id" = $1 ;` @@ -1084,16 +1078,24 @@ UPDATE panels panel.FeedbackEnabled, panel.SupportCanView, panel.SupportCanType, - panel.MessageUsesComponentsV2, - panel.MessageComponents, - panel.WelcomeMessageUsesComponentsV2, - panel.WelcomeMessageComponents, - panel.Name, ) return err } +// Kept out of UpdateWithTx so callers that don't set these fields can't clear them. +func (p *PanelTable) UpdateComponentsV2WithTx(ctx context.Context, tx pgx.Tx, panel Panel) error { + query := `UPDATE panels SET "message_uses_components_v2" = $2, "message_components" = $3, "welcome_message_uses_components_v2" = $4, "welcome_message_components" = $5 WHERE "panel_id" = $1;` + _, err := tx.Exec(ctx, query, panel.PanelId, panel.MessageUsesComponentsV2, panel.MessageComponents, panel.WelcomeMessageUsesComponentsV2, panel.WelcomeMessageComponents) + return err +} + +func (p *PanelTable) SetNameWithTx(ctx context.Context, tx pgx.Tx, panelId int, name *string) error { + query := `UPDATE panels SET "name" = $2 WHERE "panel_id" = $1;` + _, err := tx.Exec(ctx, query, panelId, name) + return err +} + func (p *PanelTable) SetOverflow(ctx context.Context, panelId int, enabled bool, categoryId *uint64) (err error) { query := `UPDATE panels SET "overflow_enabled" = $2, "overflow_category_id" = $3 WHERE "panel_id" = $1;` _, err = p.Exec(ctx, query, panelId, enabled, categoryId) diff --git a/tickets.go b/tickets.go index ba258d7..27b6662 100644 --- a/tickets.go +++ b/tickets.go @@ -1548,12 +1548,12 @@ func secondsToDuration(secs *float64) *time.Duration { func (t *TicketTable) GetTicketCountByPanel(ctx context.Context, guildId uint64, nDays int, filter *PanelFilter) ([]PanelTicketCount, error) { query := ` -SELECT t.panel_id, COALESCE(p.title, 'No Panel'), COUNT(*) +SELECT t.panel_id, COALESCE(NULLIF(p.name, ''), p.title, 'No Panel'), COUNT(*) FROM tickets t LEFT JOIN panels p ON t.panel_id = p.panel_id WHERE t.guild_id = $1 AND t.open_time > CURRENT_DATE - ($2 - 1) * INTERVAL '1 day'` + PanelPredicate("t", 3, 4) + ` -GROUP BY t.panel_id, p.title +GROUP BY t.panel_id, p.name, p.title ORDER BY COUNT(*) DESC;` arr, unassigned, err := filter.Args()