feat(modules): add an unlisted command visibility

Unlisted commands can be run by anyone like public ones but never appear
in /help or the Telegram command menu.
This commit is contained in:
tiennm99 committed 2026-10-01 15:17:11 +07:00
1 parent d2f09ecb33
commit 577c0e3a70
14 files changed
+14 -4

No files matched your search

+1
View File
@@ -24,6 +24,7 @@ func TestBotCommandMenu_UsesLoadedPublicCommandsInModuleOrder(t *testing.T) {
Commands: []modules.Command{ Commands: []modules.Command{
{Name: "beta_public", Description: "Beta public", Parameters: "<value>", Visibility: modules.VisibilityPublic}, {Name: "beta_public", Description: "Beta public", Parameters: "<value>", Visibility: modules.VisibilityPublic},
{Name: "beta_private", Description: "Beta private", Visibility: modules.VisibilityPrivate}, {Name: "beta_private", Description: "Beta private", Visibility: modules.VisibilityPrivate},
{Name: "beta_unlisted", Description: "Beta unlisted", Visibility: modules.VisibilityUnlisted},
}, },
}, },
{ {
+1 -1
View File
@@ -26,7 +26,7 @@ type Auth struct {
// Denies are silent — callers must NOT reply to denied requests, otherwise the // Denies are silent — callers must NOT reply to denied requests, otherwise the
// existence of a Protected/Private command is leaked to unprivileged users. // existence of a Protected/Private command is leaked to unprivileged users.
func (a Auth) Permits(v Visibility, update *models.Update) bool { func (a Auth) Permits(v Visibility, update *models.Update) bool {
if v == VisibilityPublic { if v == VisibilityPublic || v == VisibilityUnlisted {
return true return true
} }
if update == nil { if update == nil {
+2
View File
@@ -37,6 +37,8 @@ func TestAuth_Permits(t *testing.T) {
}{ }{
{"public-no-message", VisibilityPublic, &models.Update{}, true}, {"public-no-message", VisibilityPublic, &models.Update{}, true},
{"public-stranger", VisibilityPublic, updateFrom(stranger), 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-owner", VisibilityProtected, updateFrom(owner), true},
{"protected-callback-owner", VisibilityProtected, callbackFrom(owner), true}, {"protected-callback-owner", VisibilityProtected, callbackFrom(owner), true},
{"protected-admin", VisibilityProtected, updateFrom(admin), true}, {"protected-admin", VisibilityProtected, updateFrom(admin), true},
+4 -1
View File
@@ -12,13 +12,16 @@ import (
// Visibility classifies who may invoke a command. The dispatcher enforces // Visibility classifies who may invoke a command. The dispatcher enforces
// this at command-handler entry: Public is unrestricted; Protected requires // this at command-handler entry: Public is unrestricted; Protected requires
// the sender to be in Auth.AdminUserIDs (or be the bot owner); Private // 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 type Visibility int
const ( const (
VisibilityPublic Visibility = iota VisibilityPublic Visibility = iota
VisibilityProtected VisibilityProtected
VisibilityPrivate VisibilityPrivate
VisibilityUnlisted
) )
// CommandHandler runs in response to a Telegram command. Returning an error // CommandHandler runs in response to a Telegram command. Returning an error
+4
View File
@@ -36,6 +36,7 @@ func TestRenderHelp_GroupsByModuleAndSkipsNonPublic(t *testing.T) {
cmd("a_pub", modules.VisibilityPublic, "alpha public"), cmd("a_pub", modules.VisibilityPublic, "alpha public"),
cmd("a_prot", modules.VisibilityProtected, "alpha protected"), cmd("a_prot", modules.VisibilityProtected, "alpha protected"),
cmd("a_priv", modules.VisibilityPrivate, "alpha private — must not appear"), 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{ "beta": fakeFactory("beta", []modules.Command{
cmd("b_pub", modules.VisibilityPublic, "beta <i>desc</i>"), cmd("b_pub", modules.VisibilityPublic, "beta <i>desc</i>"),
@@ -67,6 +68,9 @@ func TestRenderHelp_GroupsByModuleAndSkipsNonPublic(t *testing.T) {
if strings.Contains(out, "a_priv") { if strings.Contains(out, "a_priv") {
t.Errorf("output leaked private command\n---output---\n%s", out) 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") { if strings.Contains(out, "a_prot") {
t.Errorf("output leaked protected command\n---output---\n%s", out) t.Errorf("output leaked protected command\n---output---\n%s", out)
} }
+2 -2
View File
@@ -16,7 +16,7 @@ func validateCommand(c Command) error {
return fmt.Errorf("command name %q must match %s", c.Name, commandNameRe) return fmt.Errorf("command name %q must match %s", c.Name, commandNameRe)
} }
switch c.Visibility { switch c.Visibility {
case VisibilityPublic, VisibilityProtected, VisibilityPrivate: case VisibilityPublic, VisibilityProtected, VisibilityPrivate, VisibilityUnlisted:
default: default:
return fmt.Errorf("command %q: unknown visibility %d", c.Name, c.Visibility) 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) return fmt.Errorf("callback prefix %q is invalid", c.Prefix)
} }
switch c.Visibility { switch c.Visibility {
case VisibilityPublic, VisibilityProtected, VisibilityPrivate: case VisibilityPublic, VisibilityProtected, VisibilityPrivate, VisibilityUnlisted:
default: default:
return fmt.Errorf("callback prefix %q: unknown visibility %d", c.Prefix, c.Visibility) return fmt.Errorf("callback prefix %q: unknown visibility %d", c.Prefix, c.Visibility)
} }