From 768dc859d73881351af601be3f8ba27e57b8616f Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Thu, 23 Apr 2026 21:50:00 +0300 Subject: [PATCH] (new): logger replacer for token (fix): scene command routing --- .gitea/workflows/go-lint.yaml | 13 +++++++ .gitea/workflows/test.yaml | 19 ---------- .github/.gitkeep | 0 CHANGELOG.md | 2 ++ bot_opts_loader.go | 9 +++++ bot_utils.go | 4 +-- go.mod | 8 ++--- go.sum | 17 +++++---- scene_handler.go | 4 +++ scene_test.go | 68 +++++++++++++++++++++++++++++++++++ 10 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 .gitea/workflows/go-lint.yaml delete mode 100644 .gitea/workflows/test.yaml delete mode 100644 .github/.gitkeep diff --git a/.gitea/workflows/go-lint.yaml b/.gitea/workflows/go-lint.yaml new file mode 100644 index 0000000..694ced1 --- /dev/null +++ b/.gitea/workflows/go-lint.yaml @@ -0,0 +1,13 @@ +name: Golang lint +run-name: Linting code in ${{ gitea.repository }} by ${{ gitea.actor }} +on: [push] + +jobs: + lint: + runs-on: go-latest + steps: + - name: Check out repository code + uses: actions/checkout@v6 + + - name: Run golangci-lint + run: golangci-lint run \ No newline at end of file diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml deleted file mode 100644 index 620fe2f..0000000 --- a/.gitea/workflows/test.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: Gitea Actions Demo -run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 -on: [push] - -jobs: - Explore-Gitea-Actions: - runs-on: arch-latest - steps: - - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." - - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" - - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." - - name: Check out repository code - uses: actions/checkout@v4 - - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." - - run: echo "🖥️ The workflow is now ready to test your code on the runner." - - name: List files in the repository - run: | - ls ${{ gitea.workspace }} - - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/.github/.gitkeep b/.github/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/CHANGELOG.md b/CHANGELOG.md index aa97221..079d16c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ - Added plugin-level message fallback handlers for text messages and channel posts that do not match commands. - Added godoc for the exported `BotOpts` file codec and load/save helpers. - README, README_RU, and bot-configuration wiki pages now document file-based `BotOpts` loading, built-in JSON support, env placeholder expansion, and custom codec usage including the TOML example. +- Active scenes now let unmatched slash-commands continue into normal bot command routing instead of also executing the current scene step or scene message fallback. ### Tests - Added regression coverage for JSON `BotOpts` file codecs, file load/save helpers, decode failures, and env placeholder expansion. - Added regression coverage for plugin message fallback routing, observer lifecycle events, command precedence, and middleware blocking. +- Added regression coverage proving unmatched slash-commands do not trigger active scene step handlers before normal bot command routing. ## v1.0.0-rc.14 diff --git a/bot_opts_loader.go b/bot_opts_loader.go index f06128e..7cee524 100644 --- a/bot_opts_loader.go +++ b/bot_opts_loader.go @@ -104,12 +104,21 @@ func (codec BotOptsFileJsonCodec) ToBytes(opts *BotOpts) ([]byte, error) { return data, nil } +func (codec BotOptsFileJsonCodec) Load(filename string) (*BotOpts, error) { + return LoadBotOptsFile(codec, filename) +} +func (codec BotOptsFileJsonCodec) Save(filename string, opts *BotOpts) error { + return SaveBotOptsFile(codec, filename, opts) +} + var envParameterRegex = regexp.MustCompile(`\{\{\s*(\w+)\s*\}\}`) // BotOptsFileCodec decodes and encodes BotOpts file formats. type BotOptsFileCodec interface { FromBytes([]byte) (*BotOpts, error) ToBytes(*BotOpts) ([]byte, error) + Load(filename string) (*BotOpts, error) + Save(filename string, opts *BotOpts) error } // LoadBotOptsFile reads a config file, expands env placeholders, and decodes BotOpts. diff --git a/bot_utils.go b/bot_utils.go index 097aa19..6bdb4f9 100644 --- a/bot_utils.go +++ b/bot_utils.go @@ -41,7 +41,7 @@ func (bot *Bot[T]) initLoggers(opts *BotOpts) { level = slog.DEBUG } - bot.logger = utils.CreateLogger("BOT", level) + bot.logger = utils.CreateLogger("BOT", level).AddReplacer(bot.token, "") if opts.WriteToFile { path := fmt.Sprintf("%s/main.log", strings.TrimRight(opts.LoggerBasePath, "/")) logger, err := utils.CreateFileLogger("BOT", level, path) @@ -53,7 +53,7 @@ func (bot *Bot[T]) initLoggers(opts *BotOpts) { } if opts.UseRequestLogger { - bot.RequestLogger = utils.CreateLogger("REQUESTS", level) + bot.RequestLogger = utils.CreateLogger("REQUESTS", level).AddReplacer(bot.token, "") if opts.WriteToFile { path := fmt.Sprintf("%s/requests.log", strings.TrimRight(opts.LoggerBasePath, "/")) logger, err := utils.CreateFileLogger("REQUESTS", level, path) diff --git a/go.mod b/go.mod index 0399919..7d1b6e3 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,14 @@ retract v1.0.0-rc.5 require ( git.scuroneko.dev/scuroneko/extypes v1.2.3 - git.scuroneko.dev/scuroneko/slog v1.1.3 + git.scuroneko.dev/scuroneko/slog v1.2.0 github.com/alitto/pond/v2 v2.7.0 golang.org/x/time v0.15.0 ) require ( - github.com/fatih/color v1.18.0 // indirect + github.com/fatih/color v1.19.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/sys v0.42.0 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + golang.org/x/sys v0.43.0 // indirect ) diff --git a/go.sum b/go.sum index 6297810..7b7f275 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,16 @@ git.scuroneko.dev/scuroneko/extypes v1.2.3 h1:n7QsfTZEn9fJNZLXGH/LkNq4cADaRk+LTu6LNMv9y6s= git.scuroneko.dev/scuroneko/extypes v1.2.3/go.mod h1:MhYpXC6sloLOpoM2guf64eSOrz+ET/QJZ8toobc3Ors= -git.scuroneko.dev/scuroneko/slog v1.1.3 h1:vI4GZykn8gDb6OJ2xq+KLcEk38M7O4e/z1kzpeRHEHw= -git.scuroneko.dev/scuroneko/slog v1.1.3/go.mod h1:gnDap54sfZv3EuSyZd7fjOH46aLbDFpvtN2wgFcWkgE= +git.scuroneko.dev/scuroneko/slog v1.2.0 h1:xbwzrMcmN0NG/zTgEn508mn2JVnfZN5z/Zsi3PREfDM= +git.scuroneko.dev/scuroneko/slog v1.2.0/go.mod h1:r+oz9NzvvdtWd9/PjeS+n5vQoNHL38BdcdLoBtJPvFU= github.com/alitto/pond/v2 v2.7.0 h1:c76L+yN916m/DRXjGCeUBHHu92uWnh/g1bwVk4zyyXg= github.com/alitto/pond/v2 v2.7.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/scene_handler.go b/scene_handler.go index 12c6f00..a70b5e5 100644 --- a/scene_handler.go +++ b/scene_handler.go @@ -81,6 +81,10 @@ func (bot *Bot[T]) executeScene(ctx *SceneContext, scene *Scene[T]) (bool, error } return ok, err } + + // Unmatched slash-commands should continue through normal bot command routing + // instead of also triggering the active scene step or fallback handler. + return false, nil } ctx.Text = text ctx.Args = nil diff --git a/scene_test.go b/scene_test.go index ea9903b..2fd57e5 100644 --- a/scene_test.go +++ b/scene_test.go @@ -509,6 +509,74 @@ func TestScenePassDoesNotPersistSessionData(t *testing.T) { } } +func TestSceneUnmatchedCommandFallsThroughWithoutRunningStep(t *testing.T) { + commandCalled := false + stepCalled := false + + plugin := NewPlugin[NoData]("wizard") + plugin.NewScene("signup"). + SetEntry("start"). + OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) { + stepCalled = true + return ctx.Stay(), nil + }) + plugin.NewCommand(func(ctx *MsgContext, db NoData) error { + commandCalled = true + return nil + }, "ping") + + bot := &Bot[NoData]{ + logger: slog.CreateLogger(), + prefixes: []string{"/"}, + sessionStore: NewMemorySessionStore(), + sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser}, + } + bot.AddPlugins(plugin) + + enterCtx := &MsgContext{ + Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}}, + FromID: 42, + sceneRuntime: bot, + } + if err := enterCtx.EnterScene("signup"); err != nil { + t.Fatalf("EnterScene returned error: %v", err) + } + + key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{ + Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}}, + FromID: 42, + }) + if !ok { + t.Fatal("expected scene key to be built") + } + + bot.handle(context.Background(), &tgapi.Update{ + UpdateID: 5, + Type: tgapi.UpdateTypeMessage, + Message: &tgapi.Message{ + MessageID: 10, + Text: "/ping", + Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}, + From: &tgapi.User{ID: 42}, + }, + }) + + if !commandCalled { + t.Fatal("expected normal command routing to handle /ping") + } + if stepCalled { + t.Fatal("scene step must not run for an unmatched slash-command") + } + + after, err := bot.sessionStore.Get(key) + if err != nil { + t.Fatalf("Get after handle returned error: %v", err) + } + if after.Scene != "signup" || after.Step != "start" { + t.Fatalf("unexpected session after command fallback: %#v", after) + } +} + func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) { fallbackCalled := false