Skip to content

feat(admin): add guildstate commands, helpers & home_guild sync#2280

Merged
mkmccarty merged 1 commit intomainfrom
mm-branch-1
Mar 22, 2026
Merged

feat(admin): add guildstate commands, helpers & home_guild sync#2280
mkmccarty merged 1 commit intomainfrom
mm-branch-1

Conversation

@mkmccarty
Copy link
Owner

No description provided.

Copilot AI review requested due to automatic review settings March 22, 2026 16:12
@mkmccarty mkmccarty merged commit 7f2fc7d into main Mar 22, 2026
12 checks passed
@mkmccarty mkmccarty deleted the mm-branch-1 branch March 22, 2026 16:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a consolidated admin entrypoint for managing persisted guildstate across guilds (with a guild ID override), plus supporting helpers and command-sync behavior driven by a home_guild setting.

Changes:

  • Add admin-guildstate slash command with action + guild-id override and autocomplete sourcing from persisted guildstate.
  • Refactor guildstate admin handlers into *ForGuild(...) variants to support explicit guild overrides.
  • Add home_guild-driven command sync splitting and a helper to list persisted guild IDs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
src/guildstate/state.go Adds GetAllGuildIDs() helper used for admin autocomplete.
src/guildstate/slashcmd.go Refactors handlers to support explicit guildID overrides and adds display-name helper.
src/boost/boost_admin.go Introduces admin-guildstate command, routing, and autocomplete.
main.go Splits command syncing to optionally sync some commands to home_guild.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +84 to +86
if guildID == "" {
guildID = "DISABLED"
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SlashAdminGuildStateCommand sets GuildID to the literal string "DISABLED" when home_guild is unset. Because main.go then filters commands by cmd.GuildID == homeGuild, this command is effectively never synced/registered unless home_guild is configured. Consider either (a) not adding this command to adminCommands unless home_guild is a valid snowflake, or (b) leaving GuildID empty and skipping the home-guild special casing when unset, rather than using a sentinel that isn’t a valid Discord guild ID.

Suggested change
if guildID == "" {
guildID = "DISABLED"
}

Copilot uses AI. Check for mistakes.
if err != nil {
log.Println(err)
}
return perms&discordgo.PermissionAdministrator != 0 || userID == config.AdminUserID
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The permission check here allows any server administrator in the command’s invocation channel to run cross-guild state changes (because the command accepts a guild-id override). If the bot is in multiple guilds, this effectively lets admins of the "home" guild modify settings for other guilds. Consider restricting cross-guild actions to config.AdminUserID (or an explicit allowlist) instead of PermissionAdministrator.

Suggested change
return perms&discordgo.PermissionAdministrator != 0 || userID == config.AdminUserID
return userID == config.AdminUserID

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +163
if guild, guildErr := s.Guild(id); guildErr == nil && guild != nil {
guildName := strings.TrimSpace(guild.Name)
if guildName != "" {
choiceName = fmt.Sprintf("%s (%s)", guildName, id)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandleAdminGuildStateAutoComplete calls s.Guild(id) inside the loop. If this is a REST fetch (discordgo’s Session.Guild), autocomplete can trigger up to 25 network calls per keystroke, which is likely to hit rate limits and slow down the UI. Prefer using the state cache (e.g., s.State.Guild) or returning IDs without resolving names (or caching resolved names).

Suggested change
if guild, guildErr := s.Guild(id); guildErr == nil && guild != nil {
guildName := strings.TrimSpace(guild.Name)
if guildName != "" {
choiceName = fmt.Sprintf("%s (%s)", guildName, id)
if s.State != nil {
if guild, guildErr := s.State.Guild(id); guildErr == nil && guild != nil {
guildName := strings.TrimSpace(guild.Name)
if guildName != "" {
choiceName = fmt.Sprintf("%s (%s)", guildName, id)
}

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +222
if guildID == "" {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "guild-id is required.",
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only checking guildID != "" means callers can type any snowflake and create/update persisted state for a guild the bot isn’t in (or that never existed). Since this command is explicitly about operating on persisted guildstate, consider validating guild-id against guildstate.GetAllGuildIDs() (or ensuring s.Guild(guildID) succeeds) and rejecting unknown guild IDs.

Copilot uses AI. Check for mistakes.
respondEphemeral(s, i, "Guild ID is required.")
return
}

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetGuildSettingForGuild will persist settings for any non-empty guildID, even if the bot is not in that guild / it’s not a known persisted guild. Since this is now used with an explicit guild override, consider validating the provided guildID (e.g., ensure it exists in persisted guildstate or that s.Guild(guildID) succeeds) before writing.

Suggested change
// Validate that the guild ID corresponds to a guild the bot can access
if dgGuild, guildErr := s.Guild(guildID); guildErr != nil || dgGuild == nil {
respondEphemeral(s, i, fmt.Sprintf("Guild ID '%s' is not valid or the bot is not in that guild.", guildID))
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +1103 to 1106
if homeGuild != "DISABLED" {
syncCommands(s, homeGuild, homeGuildCommandSet)
}
syncCommands(s, config.DiscordGuildID, commandSet)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If homeGuild equals config.DiscordGuildID, this code will call syncCommands twice for the same guild with two different desired command sets. The second call will delete commands from the first call (because syncCommands bulk-overwrites), so the "home guild" commands won’t actually stick. Consider merging the two desired lists (or skipping the split) when the guild IDs match.

Suggested change
if homeGuild != "DISABLED" {
syncCommands(s, homeGuild, homeGuildCommandSet)
}
syncCommands(s, config.DiscordGuildID, commandSet)
// Avoid calling syncCommands twice for the same guild with different command sets.
// If homeGuild is enabled and equals the main DiscordGuildID, merge the two sets and sync once.
if homeGuild != "DISABLED" && homeGuild == config.DiscordGuildID {
mergedCommandSet := make([]*discordgo.ApplicationCommand, 0, len(commandSet)+len(homeGuildCommandSet))
mergedCommandSet = append(mergedCommandSet, commandSet...)
mergedCommandSet = append(mergedCommandSet, homeGuildCommandSet...)
syncCommands(s, config.DiscordGuildID, mergedCommandSet)
} else {
if homeGuild != "DISABLED" {
syncCommands(s, homeGuild, homeGuildCommandSet)
}
syncCommands(s, config.DiscordGuildID, commandSet)
}

Copilot uses AI. Check for mistakes.
Comment on lines +1087 to +1099
homeGuild := guildstate.GetGuildSettingString("DEFAULT", "home_guild")
if homeGuild == "" {
homeGuild = "DISABLED"
}

var homeGuildCommandSet []*discordgo.ApplicationCommand
var filteredCommandSet []*discordgo.ApplicationCommand
for _, cmd := range commandSet {
if homeGuild != "" && cmd.GuildID == homeGuild {
homeGuildCommandSet = append(homeGuildCommandSet, cmd)
} else {
filteredCommandSet = append(filteredCommandSet, cmd)
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting homeGuild to the sentinel "DISABLED" makes homeGuild != "" always true, so any command with cmd.GuildID == "DISABLED" gets filtered out of commandSet (and then never synced because homeGuild != "DISABLED" gates the home sync). If the intent is to disable home-guild syncing when unset, it’s clearer/safer to keep homeGuild empty and branch explicitly (or avoid assigning an invalid guild ID to commands).

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +156
items, err := GetAllGuildState()
if err != nil {
return nil, err
}

ids := make([]string, 0, len(items))
seen := make(map[string]struct{}, len(items))
for _, item := range items {
if item.GuildID == "" {
continue
}
if _, ok := seen[item.GuildID]; ok {
continue
}
seen[item.GuildID] = struct{}{}
ids = append(ids, item.GuildID)
}

sort.Strings(ids)
return ids, nil
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetAllGuildIDs currently calls GetAllGuildState(), which selects * (including the value JSON) and unmarshals each row just to extract IDs. Since guild_record.id is a PRIMARY KEY, you can fetch and sort IDs without loading/parsing value (e.g., add a SELECT id FROM guild_record sqlc query) and drop the seen de-dupe map (duplicates can’t occur). This will make autocomplete lighter as the table grows.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants