diff --git a/cmd/server/command_menu_test.go b/cmd/server/command_menu_test.go index 4bef2e4..668ead6 100644 --- a/cmd/server/command_menu_test.go +++ b/cmd/server/command_menu_test.go @@ -24,6 +24,7 @@ func TestBotCommandMenu_UsesLoadedPublicCommandsInModuleOrder(t *testing.T) { Commands: []modules.Command{ {Name: "beta_public", Description: "Beta public", Parameters: "", Visibility: modules.VisibilityPublic}, {Name: "beta_private", Description: "Beta private", Visibility: modules.VisibilityPrivate}, + {Name: "beta_unlisted", Description: "Beta unlisted", Visibility: modules.VisibilityUnlisted}, }, }, { diff --git a/internal/modules/dispatcher.go b/internal/modules/dispatcher.go index 63357e3..9315fd1 100644 --- a/internal/modules/dispatcher.go +++ b/internal/modules/dispatcher.go @@ -26,7 +26,7 @@ type Auth struct { // Denies are silent — callers must NOT reply to denied requests, otherwise the // existence of a Protected/Private command is leaked to unprivileged users. func (a Auth) Permits(v Visibility, update *models.Update) bool { - if v == VisibilityPublic { + if v == VisibilityPublic || v == VisibilityUnlisted { return true } if update == nil { diff --git a/internal/modules/dispatcher_test.go b/internal/modules/dispatcher_test.go index 35b0963..49a609d 100644 --- a/internal/modules/dispatcher_test.go +++ b/internal/modules/dispatcher_test.go @@ -37,6 +37,8 @@ func TestAuth_Permits(t *testing.T) { }{ {"public-no-message", VisibilityPublic, &models.Update{}, true}, {"public-stranger", VisibilityPublic, updateFrom(stranger), true}, + {"unlisted-stranger", VisibilityUnlisted, updateFrom(stranger), true}, + {"unlisted-no-message", VisibilityUnlisted, &models.Update{}, true}, {"protected-owner", VisibilityProtected, updateFrom(owner), true}, {"protected-callback-owner", VisibilityProtected, callbackFrom(owner), true}, {"protected-admin", VisibilityProtected, updateFrom(admin), true}, diff --git a/internal/modules/module.go b/internal/modules/module.go index 7052a68..e05fee1 100644 --- a/internal/modules/module.go +++ b/internal/modules/module.go @@ -12,13 +12,16 @@ import ( // Visibility classifies who may invoke a command. The dispatcher enforces // this at command-handler entry: Public is unrestricted; Protected requires // the sender to be in Auth.AdminUserIDs (or be the bot owner); Private -// requires the sender to be Auth.BotOwnerID. /help filters by the same field. +// requires the sender to be Auth.BotOwnerID; Unlisted is unrestricted like +// Public but never advertised. /help and the Telegram command menu list +// Public commands only. type Visibility int const ( VisibilityPublic Visibility = iota VisibilityProtected VisibilityPrivate + VisibilityUnlisted ) // CommandHandler runs in response to a Telegram command. Returning an error diff --git a/internal/modules/misc/gacha_api_client.go b/internal/modules/random/gacha_api_client.go similarity index 100% rename from internal/modules/misc/gacha_api_client.go rename to internal/modules/random/gacha_api_client.go diff --git a/internal/modules/misc/gacha_api_client_test.go b/internal/modules/random/gacha_api_client_test.go similarity index 100% rename from internal/modules/misc/gacha_api_client_test.go rename to internal/modules/random/gacha_api_client_test.go diff --git a/internal/modules/misc/gacha_command.go b/internal/modules/random/gacha_command.go similarity index 100% rename from internal/modules/misc/gacha_command.go rename to internal/modules/random/gacha_command.go diff --git a/internal/modules/misc/gacha_command_test.go b/internal/modules/random/gacha_command_test.go similarity index 100% rename from internal/modules/misc/gacha_command_test.go rename to internal/modules/random/gacha_command_test.go diff --git a/internal/modules/misc/random_picker.go b/internal/modules/random/random_picker.go similarity index 100% rename from internal/modules/misc/random_picker.go rename to internal/modules/random/random_picker.go diff --git a/internal/modules/misc/wheelofnames_api_client.go b/internal/modules/random/wheelofnames_api_client.go similarity index 100% rename from internal/modules/misc/wheelofnames_api_client.go rename to internal/modules/random/wheelofnames_api_client.go diff --git a/internal/modules/misc/wheelofnames_api_client_test.go b/internal/modules/random/wheelofnames_api_client_test.go similarity index 100% rename from internal/modules/misc/wheelofnames_api_client_test.go rename to internal/modules/random/wheelofnames_api_client_test.go diff --git a/internal/modules/misc/wheelofnames_command.go b/internal/modules/random/wheelofnames_command.go similarity index 100% rename from internal/modules/misc/wheelofnames_command.go rename to internal/modules/random/wheelofnames_command.go diff --git a/internal/modules/util/help_test.go b/internal/modules/util/help_test.go index 0d9baac..759955d 100644 --- a/internal/modules/util/help_test.go +++ b/internal/modules/util/help_test.go @@ -36,6 +36,7 @@ func TestRenderHelp_GroupsByModuleAndSkipsNonPublic(t *testing.T) { cmd("a_pub", modules.VisibilityPublic, "alpha public"), cmd("a_prot", modules.VisibilityProtected, "alpha protected"), cmd("a_priv", modules.VisibilityPrivate, "alpha private — must not appear"), + cmd("a_unlisted", modules.VisibilityUnlisted, "alpha unlisted — must not appear"), }), "beta": fakeFactory("beta", []modules.Command{ cmd("b_pub", modules.VisibilityPublic, "beta desc"), @@ -67,6 +68,9 @@ func TestRenderHelp_GroupsByModuleAndSkipsNonPublic(t *testing.T) { if strings.Contains(out, "a_priv") { t.Errorf("output leaked private command\n---output---\n%s", out) } + if strings.Contains(out, "a_unlisted") { + t.Errorf("output leaked unlisted command\n---output---\n%s", out) + } if strings.Contains(out, "a_prot") { t.Errorf("output leaked protected command\n---output---\n%s", out) } diff --git a/internal/modules/validate.go b/internal/modules/validate.go index ce6de82..496a024 100644 --- a/internal/modules/validate.go +++ b/internal/modules/validate.go @@ -16,7 +16,7 @@ func validateCommand(c Command) error { return fmt.Errorf("command name %q must match %s", c.Name, commandNameRe) } switch c.Visibility { - case VisibilityPublic, VisibilityProtected, VisibilityPrivate: + case VisibilityPublic, VisibilityProtected, VisibilityPrivate, VisibilityUnlisted: default: return fmt.Errorf("command %q: unknown visibility %d", c.Name, c.Visibility) } @@ -56,7 +56,7 @@ func validateCallback(c Callback) error { return fmt.Errorf("callback prefix %q is invalid", c.Prefix) } switch c.Visibility { - case VisibilityPublic, VisibilityProtected, VisibilityPrivate: + case VisibilityPublic, VisibilityProtected, VisibilityPrivate, VisibilityUnlisted: default: return fmt.Errorf("callback prefix %q: unknown visibility %d", c.Prefix, c.Visibility) }