-
Notifications
You must be signed in to change notification settings - Fork 3
feat(admin): add guildstate commands, helpers & home_guild sync #2280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -46,6 +46,7 @@ const slashAdminGetContractData string = "admin-get-contract-data" | |||||||||||||||||||||||||||||||||||
| const slashAdminListRoles string = "list-roles" | ||||||||||||||||||||||||||||||||||||
| const slashAdminSetGuildSetting string = "admin-set-guild-setting" | ||||||||||||||||||||||||||||||||||||
| const slashAdminGetGuildSettings string = "admin-get-guild-settings" | ||||||||||||||||||||||||||||||||||||
| const slashAdminGuildstate string = "admin-guildstate" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Slash Command Constants | ||||||||||||||||||||||||||||||||||||
| const slashContract string = "contract" | ||||||||||||||||||||||||||||||||||||
|
|
@@ -210,6 +211,7 @@ var ( | |||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| boost.SlashAdminGetContractData(slashAdminGetContractData), | ||||||||||||||||||||||||||||||||||||
| boost.SlashAdminListRoles(slashAdminListRoles), | ||||||||||||||||||||||||||||||||||||
| boost.SlashAdminGuildStateCommand(slashAdminGuildstate), | ||||||||||||||||||||||||||||||||||||
| guildstate.SlashSetGuildSettingCommand(slashAdminSetGuildSetting), | ||||||||||||||||||||||||||||||||||||
| guildstate.SlashGetGuildSettingsCommand(slashAdminGetGuildSettings), | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
@@ -385,6 +387,9 @@ var ( | |||||||||||||||||||||||||||||||||||
| slashAdminListRoles: func(s *discordgo.Session, i *discordgo.InteractionCreate) { | ||||||||||||||||||||||||||||||||||||
| boost.HandleContractAutoComplete(s, i) | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| slashAdminGuildstate: func(s *discordgo.Session, i *discordgo.InteractionCreate) { | ||||||||||||||||||||||||||||||||||||
| boost.HandleAdminGuildStateAutoComplete(s, i) | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| slashToken: func(s *discordgo.Session, i *discordgo.InteractionCreate) { | ||||||||||||||||||||||||||||||||||||
| boost.HandleContractAutoComplete(s, i) | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
|
|
@@ -522,6 +527,9 @@ var ( | |||||||||||||||||||||||||||||||||||
| slashAdminListRoles: func(s *discordgo.Session, i *discordgo.InteractionCreate) { | ||||||||||||||||||||||||||||||||||||
| boost.HandleAdminListRoles(s, i) | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| slashAdminGuildstate: func(s *discordgo.Session, i *discordgo.InteractionCreate) { | ||||||||||||||||||||||||||||||||||||
| boost.HandleAdminGuildStateCommand(s, i) | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| slashAdminSetGuildSetting: func(s *discordgo.Session, i *discordgo.InteractionCreate) { | ||||||||||||||||||||||||||||||||||||
| guildstate.SetGuildSetting(s, i) | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
|
|
@@ -1075,6 +1083,26 @@ func main() { | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| commandSet = append(commandSet, adminCommands...) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Restrict some commands to a specific guild if the home_guild setting is set, to allow for faster iteration during development without affecting the global command set that everyone uses | ||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| commandSet = filteredCommandSet | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if homeGuild != "DISABLED" { | ||||||||||||||||||||||||||||||||||||
| syncCommands(s, homeGuild, homeGuildCommandSet) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| syncCommands(s, config.DiscordGuildID, commandSet) | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1103
to
1106
|
||||||||||||||||||||||||||||||||||||
| 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) | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,7 +14,14 @@ import ( | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| "github.com/bwmarrin/discordgo" | ||||||||||||||||||||||
| "github.com/mkmccarty/TokenTimeBoostBot/src/bottools" | ||||||||||||||||||||||
| "github.com/mkmccarty/TokenTimeBoostBot/src/config" | ||||||||||||||||||||||
| "github.com/mkmccarty/TokenTimeBoostBot/src/ei" | ||||||||||||||||||||||
| "github.com/mkmccarty/TokenTimeBoostBot/src/guildstate" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const ( | ||||||||||||||||||||||
| adminGuildStateActionSet = "set-guild-setting" | ||||||||||||||||||||||
| adminGuildStateActionGet = "get-guild-settings" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // SlashAdminGetContractData is the slash to get contract JSON data | ||||||||||||||||||||||
|
|
@@ -69,6 +76,185 @@ func SlashAdminListRoles(cmd string) *discordgo.ApplicationCommand { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // SlashAdminGuildStateCommand provides a generic entrypoint for guildstate admin actions. | ||||||||||||||||||||||
| func SlashAdminGuildStateCommand(cmd string) *discordgo.ApplicationCommand { | ||||||||||||||||||||||
| var adminPermission = int64(0) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| guildID := guildstate.GetGuildSettingString("DEFAULT", "home_guild") | ||||||||||||||||||||||
| if guildID == "" { | ||||||||||||||||||||||
| guildID = "DISABLED" | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+84
to
+86
|
||||||||||||||||||||||
| if guildID == "" { | |
| guildID = "DISABLED" | |
| } |
Copilot
AI
Mar 22, 2026
There was a problem hiding this comment.
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.
| return perms&discordgo.PermissionAdministrator != 0 || userID == config.AdminUserID | |
| return userID == config.AdminUserID |
Copilot
AI
Mar 22, 2026
There was a problem hiding this comment.
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).
| 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
AI
Mar 22, 2026
There was a problem hiding this comment.
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.
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -166,6 +166,14 @@ func classifySnowflake(s *discordgo.Session, guildID, id string) string { | |||||||||||||||
| return fmt.Sprintf("user (%s)", name) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if g, err := s.Guild(id); err == nil && g != nil { | ||||||||||||||||
| name := strings.TrimSpace(g.Name) | ||||||||||||||||
| if name == "" { | ||||||||||||||||
| return "guild" | ||||||||||||||||
| } | ||||||||||||||||
| return fmt.Sprintf("guild (%s)", name) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return "unknown snowflake" | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -199,36 +207,33 @@ func splitCSV(value string) []string { | |||||||||||||||
| return items | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // SetGuildSetting handles the admin slash command for setting or clearing guild settings. | ||||||||||||||||
| func SetGuildSetting(s *discordgo.Session, i *discordgo.InteractionCreate) { | ||||||||||||||||
| func getGuildDisplayName(s *discordgo.Session, guildID string) string { | ||||||||||||||||
| guildName := guildID | ||||||||||||||||
| if dgGuild, guildErr := s.Guild(guildID); guildErr == nil && dgGuild != nil { | ||||||||||||||||
| if strings.TrimSpace(dgGuild.Name) != "" { | ||||||||||||||||
| guildName = dgGuild.Name | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| return guildName | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // SetGuildSettingForGuild sets or clears a guild setting for a specific guild ID. | ||||||||||||||||
| func SetGuildSettingForGuild(s *discordgo.Session, i *discordgo.InteractionCreate, guildID, setting, value string) { | ||||||||||||||||
| if !isAdminCaller(s, i) { | ||||||||||||||||
| respondEphemeral(s, i, "You are not authorized to use this command.") | ||||||||||||||||
| return | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| optionMap := bottools.GetCommandOptionsMap(i) | ||||||||||||||||
| guildID := i.GuildID | ||||||||||||||||
| setting := "" | ||||||||||||||||
| value := "" | ||||||||||||||||
|
|
||||||||||||||||
| if opt, ok := optionMap["setting"]; ok { | ||||||||||||||||
| setting = strings.TrimSpace(opt.StringValue()) | ||||||||||||||||
| } | ||||||||||||||||
| if opt, ok := optionMap["value"]; ok { | ||||||||||||||||
| value = strings.TrimSpace(opt.StringValue()) | ||||||||||||||||
| } | ||||||||||||||||
| guildID = strings.TrimSpace(guildID) | ||||||||||||||||
| setting = strings.TrimSpace(setting) | ||||||||||||||||
| value = strings.TrimSpace(value) | ||||||||||||||||
|
|
||||||||||||||||
| if guildID == "" { | ||||||||||||||||
| respondEphemeral(s, i, "This command can only be used in a server.") | ||||||||||||||||
| respondEphemeral(s, i, "Guild ID is required.") | ||||||||||||||||
| return | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
||||||||||||||||
| // 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 | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting
homeGuildto the sentinel "DISABLED" makeshomeGuild != ""always true, so any command withcmd.GuildID == "DISABLED"gets filtered out ofcommandSet(and then never synced becausehomeGuild != "DISABLED"gates the home sync). If the intent is to disable home-guild syncing when unset, it’s clearer/safer to keephomeGuildempty and branch explicitly (or avoid assigning an invalid guild ID to commands).