From 9e95db285174ffa7012629a5cd28859fed1bcb84 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 9 May 2026 12:19:16 +0700 Subject: [PATCH] refactor(modules): allow hyphens in module names Relax module name regex to accept hyphens, preparing for hyphenated loldle variants (loldle-emoji, loldle-quote, etc.) ported from upstream JS sources. Storage prefix delimiter ':' remains rejected. Telegram command names use separate stricter regex (commandNameRe) and are unaffected. --- internal/modules/registry.go | 14 ++++++++++---- internal/modules/registry_test.go | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/internal/modules/registry.go b/internal/modules/registry.go index a9679ab..0822301 100644 --- a/internal/modules/registry.go +++ b/internal/modules/registry.go @@ -2,15 +2,21 @@ package modules import ( "fmt" + "regexp" "sort" "github.com/tiennm99/miti99bot-go/internal/storage" ) -// moduleNameRe enforces the same alphabet as command names so KV prefix -// isolation is preserved (no ":" in module names → no prefix collision) and -// the cron route's path segment regex matches. -var moduleNameRe = commandNameRe // alias kept for symmetry; one regex serves both +// moduleNameRe is intentionally looser than commandNameRe — it allows hyphen +// so modules can keep their JS-source names verbatim (e.g. "loldle-emoji"). +// The crucial constraint is "no `:`" so the storage Prefixed wrapper's `:` +// delimiter cannot be subverted; everything else is style. +// +// Telegram command names still need the stricter [a-z0-9_]{1,32} alphabet +// (commandNameRe in validate.go). Cron route segments use their own regex in +// internal/server/router.go and stay strict for log-injection safety. +var moduleNameRe = regexp.MustCompile(`^[a-z0-9_-]{1,32}$`) // Registry holds the resolved set of modules selected by the MODULES env var. // It is built once at startup; Build fails fast on validation or conflict. diff --git a/internal/modules/registry_test.go b/internal/modules/registry_test.go index bdf07e4..aa00151 100644 --- a/internal/modules/registry_test.go +++ b/internal/modules/registry_test.go @@ -210,7 +210,10 @@ func TestDispatchScheduled_PassesPrefixedDeps(t *testing.T) { } func TestBuild_RejectsInvalidModuleName(t *testing.T) { - for _, name := range []string{"BadName", "with-dash", "a:b", ""} { + // `-` is intentionally allowed (loldle-emoji and friends carry hyphenated + // names from the JS source). `:` must stay rejected — it's the storage + // prefix delimiter and a hyphen-allowing regex must not let it through. + for _, name := range []string{"BadName", "a:b", "", "with space", "with.dot", "with/slash"} { t.Run(name, func(t *testing.T) { _, err := Build([]string{name}, map[string]Factory{}, newProvider(), nil) if err == nil { @@ -220,6 +223,19 @@ func TestBuild_RejectsInvalidModuleName(t *testing.T) { } } +func TestBuild_AcceptsHyphenatedModuleName(t *testing.T) { + factories := map[string]Factory{ + "loldle-emoji": factory("loldle-emoji", []Command{noopCmd("emoji_cmd")}, nil), + } + reg, err := Build([]string{"loldle-emoji"}, factories, newProvider(), nil) + if err != nil { + t.Fatalf("hyphenated name should be allowed: %v", err) + } + if len(reg.Modules) != 1 || reg.Modules[0].Name != "loldle-emoji" { + t.Errorf("module not registered correctly: %+v", reg.Modules) + } +} + func TestBuild_RejectsDuplicateModuleInEnv(t *testing.T) { factories := map[string]Factory{ "alpha": factory("alpha", []Command{noopCmd("a1")}, nil),