REPOSITORY / ScuroNeko/Laniakea
Compare commits
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f74496a3e8
|
||
|
|
a4d70e1510
|
+7
-9
@@ -8,23 +8,19 @@
|
|||||||
- `Bot.GetPayloadType()`, `InlineKeyboard.GetPayloadType()`, and optional strict payload decoding via `BotOpts.StrictPayloadType` / `Bot.SetStrictPayloadType(...)`.
|
- `Bot.GetPayloadType()`, `InlineKeyboard.GetPayloadType()`, and optional strict payload decoding via `BotOpts.StrictPayloadType` / `Bot.SetStrictPayloadType(...)`.
|
||||||
- `MsgContext.BindArgs(...)` for binding positional command arguments into exported struct fields.
|
- `MsgContext.BindArgs(...)` for binding positional command arguments into exported struct fields.
|
||||||
- Binding sentinels `ErrBindArgsTargetNotPointer`, `ErrBindArgsTargetNotStruct`, `ErrBindArgsUnsupportedFieldType`, and `ErrBindArgsConversion`.
|
- Binding sentinels `ErrBindArgsTargetNotPointer`, `ErrBindArgsTargetNotStruct`, `ErrBindArgsUnsupportedFieldType`, and `ErrBindArgsConversion`.
|
||||||
|
- Work-in-progress scene/session support, including plugin scene registration, scoped scene sessions, scene entry/exit APIs on `MsgContext`, default in-memory session storage, scene-local routing before normal command handling, and state helpers on `SceneContext`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- `CommandExecutor` now returns `error`, and command, payload, and non-command update handlers now use centralized bot error handling for returned errors.
|
- `CommandExecutor` now returns `error`, and command, payload, and non-command update handlers now use centralized bot error handling for returned errors.
|
||||||
- README and README_RU examples now use the new handler signature and document the long-message helpers.
|
- README and README_RU examples now use the new handler signature and document the long-message helpers.
|
||||||
- README and README_RU now link to the project wiki, and the wiki now includes a page-priority tracker while content is being filled in.
|
- README and README_RU now link to the project wiki, and the wiki now includes a page-priority tracker while content is being filled in.
|
||||||
- `AGENTS.md` now requires every change to be recorded in `CHANGELOG.md`, enforces version alignment with `utils/version.go`, and blocks breaking changes without a major-version bump.
|
- README and README_RU now document scenes, session scopes, scene state helpers, and `SceneActionPass` semantics.
|
||||||
- `AGENTS.md` now also defines a short commit-message format: one summary line plus up to three high-signal detail lines.
|
- `TODO.md` and the framework backlog pages now group the remaining framework work into explicit priority 1, 2, and 3 buckets.
|
||||||
- `AGENTS.md` now explicitly requires each commit-message detail line to be placed on its own new line.
|
|
||||||
- `AGENTS.md` now also requires commit messages to be emitted as a plain multiline block instead of collapsed prose or list formatting.
|
|
||||||
- `AGENTS.md` now requires new or expanded project documentation to be maintained in both English and Russian whenever reasonably possible.
|
|
||||||
- `AGENTS.md` now requires all agent-created commits to be GPG-signed and to fail fast instead of falling back to unsigned commits when signing cannot be completed.
|
|
||||||
- `AGENTS.md` now also links the wiki backlog flow more tightly to `TODO.md` and `CHANGELOG.md`, requiring draft-wiki confirmation for large new ideas and synchronized completion records for backlog items.
|
|
||||||
- Added `TODO.md` to track missing framework-level concepts, with detailed notes for scenes, typed handler input, and request-scoped cancellation.
|
|
||||||
- Payload-type comments and docs now distinguish between the bot's default payload type and keyboard-local overrides.
|
- Payload-type comments and docs now distinguish between the bot's default payload type and keyboard-local overrides.
|
||||||
|
- Scene runtime sentinel errors now have explicit godoc comments.
|
||||||
|
- Public scene structs now document their exported fields more explicitly.
|
||||||
- `MsgContext.Context()` now safely falls back to `context.Background()` when no request-scoped context is attached.
|
- `MsgContext.Context()` now safely falls back to `context.Background()` when no request-scoped context is attached.
|
||||||
- `MsgContext` reply, edit, callback, delete, action, and draft-limiter paths now use the context accessor instead of reaching into raw internal state.
|
- `MsgContext` reply, edit, callback, delete, action, and draft-limiter paths now use the context accessor instead of reaching into raw internal state.
|
||||||
- `TODO.md` is now a short pointer file, while the detailed framework backlog lives in the wiki as `Framework-Backlog` / `Framework-Backlog-RU`.
|
|
||||||
- Version constants were bumped to `v1.0.0-rc.12`.
|
- Version constants were bumped to `v1.0.0-rc.12`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -41,6 +37,8 @@
|
|||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
- Added regression tests for `MsgContext.BindArgs(...)`, including scalar conversion, tail-string binding, zero-value trailing fields, invalid targets, unsupported field types, and end-to-end command/payload binding.
|
- Added regression tests for `MsgContext.BindArgs(...)`, including scalar conversion, tail-string binding, zero-value trailing fields, invalid targets, unsupported field types, and end-to-end command/payload binding.
|
||||||
|
- Added scene regression tests for runtime guards, scene-local command handling, and `SceneActionPass` preserving session state.
|
||||||
|
- Added scene regression tests for message fallback handling, user-scoped session lookup without `Msg`, and custom `SessionStore` error propagation.
|
||||||
|
|
||||||
## v1.0.0-rc.11
|
## v1.0.0-rc.11
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
|
|
||||||
[](https://go.dev/)
|
[](https://go.dev/)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||

|

|
||||||
|
|
||||||
A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It simplifies bot development with a clean plugin system, middleware support, automatic command generation, and built-in rate limiting.
|
A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It simplifies bot development with a clean plugin system, middleware support, automatic command generation, and built-in rate limiting.
|
||||||
|
|
||||||
[На русском](README_RU.md)
|
[На русском](README_RU.md)
|
||||||
|
|
||||||
[Wiki](https://git.nix13.pw/ScuroNeko/Laniakea/wiki)
|
[Wiki](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It s
|
|||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get git.nix13.pw/scuroneko/laniakea
|
go get git.scuroneko.dev/scuroneko/laniakea
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
@@ -47,7 +47,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea" // Import the Laniakea library
|
"git.scuroneko.dev/scuroneko/laniakea" // Import the Laniakea library
|
||||||
)
|
)
|
||||||
|
|
||||||
// echo is a command handler function.
|
// echo is a command handler function.
|
||||||
@@ -182,6 +182,43 @@ if err != nil {
|
|||||||
bot.DatabaseContext(db)
|
bot.DatabaseContext(db)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Scenes and Sessions
|
||||||
|
|
||||||
|
Scenes model multi-step conversations inside a plugin. Each active scene is stored in a session keyed by scope, so you can isolate flows per user, per chat, or per user-chat pair.
|
||||||
|
|
||||||
|
```go
|
||||||
|
plugin := laniakea.NewPlugin[MyDB]("signup")
|
||||||
|
|
||||||
|
plugin.NewScene("signup").
|
||||||
|
SetScope(laniakea.SceneScopeUserChat).
|
||||||
|
SetEntry("ask_name").
|
||||||
|
OnStep("ask_name", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
|
||||||
|
if ctx.Text == "" {
|
||||||
|
ctx.Answer("What is your name?")
|
||||||
|
return ctx.Stay(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.SaveData(struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{Name: ctx.Text}); err != nil {
|
||||||
|
return laniakea.SceneResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Answer("Nice to meet you.")
|
||||||
|
return ctx.Next("done"), nil
|
||||||
|
}).
|
||||||
|
OnStep("done", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
|
||||||
|
return ctx.Exit(), nil
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use `ctx.EnterScene("signup")` to enter the configured entry step.
|
||||||
|
- Use `ctx.EnterSceneStep("signup", "done")` when you need an explicit starting step.
|
||||||
|
- Return `ctx.Stay()`, `ctx.Next(step)`, `ctx.Exit()`, or `ctx.Pass()` from scene handlers to control flow.
|
||||||
|
- `SceneActionPass` keeps the current session unchanged and continues normal bot routing.
|
||||||
|
- Use `SceneContext.SaveData(...)` and `SceneContext.BindData(...)` for JSON session state.
|
||||||
|
- Use `SceneScopeUser`, `SceneScopeChat`, or `SceneScopeUserChat` depending on how widely a conversation should be shared.
|
||||||
|
|
||||||
## 🧩 Middleware
|
## 🧩 Middleware
|
||||||
Middleware are functions that run before a command handler. They are perfect for cross-cutting concerns like logging, access control, rate limiting, or modifying the context.
|
Middleware are functions that run before a command handler. They are perfect for cross-cutting concerns like logging, access control, rate limiting, or modifying the context.
|
||||||
|
|
||||||
@@ -247,9 +284,9 @@ func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool {
|
|||||||
This project is licensed under the GNU General Public License v3.0 — see the [LICENSE](LICENSE) file for details.
|
This project is licensed under the GNU General Public License v3.0 — see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
## 📚 Learn More
|
## 📚 Learn More
|
||||||
[GoDoc](https://pkg.go.dev/git.nix13.pw/scuroneko/laniakea)
|
[GoDoc](https://pkg.go.dev/git.scuroneko.dev/scuroneko/laniakea)
|
||||||
|
|
||||||
[Wiki](https://git.nix13.pw/ScuroNeko/Laniakea/wiki)
|
[Wiki](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki)
|
||||||
|
|
||||||
[Telegram Bot API](https://core.telegram.org/bots/api)
|
[Telegram Bot API](https://core.telegram.org/bots/api)
|
||||||
|
|
||||||
|
|||||||
+43
-6
@@ -4,13 +4,13 @@
|
|||||||
|
|
||||||
[](https://go.dev/)
|
[](https://go.dev/)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||

|

|
||||||
|
|
||||||
Легковесная, простая в использовании и производительная обёртка для Telegram Bot API на Go. Она упрощает разработку ботов благодаря чистой системе плагинов, поддержке Middleware, автоматической генерации команд и встроенному рейтлимитеру.
|
Легковесная, простая в использовании и производительная обёртка для Telegram Bot API на Go. Она упрощает разработку ботов благодаря чистой системе плагинов, поддержке Middleware, автоматической генерации команд и встроенному рейтлимитеру.
|
||||||
|
|
||||||
[English](README.md)
|
[English](README.md)
|
||||||
|
|
||||||
[Wiki](https://git.nix13.pw/ScuroNeko/Laniakea/wiki)
|
[Wiki](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
## 📦 Установка
|
## 📦 Установка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get git.nix13.pw/scuroneko/laniakea
|
go get git.scuroneko.dev/scuroneko/laniakea
|
||||||
```
|
```
|
||||||
|
|
||||||
или
|
или
|
||||||
@@ -48,7 +48,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea" // Импортируем библиотеку Laniakea
|
"git.scuroneko.dev/scuroneko/laniakea" // Импортируем библиотеку Laniakea
|
||||||
)
|
)
|
||||||
|
|
||||||
// echo — это функция-обработчик команды.
|
// echo — это функция-обработчик команды.
|
||||||
@@ -170,6 +170,43 @@ if err != nil {
|
|||||||
bot.DatabaseContext(db)
|
bot.DatabaseContext(db)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Сцены и сессии (Scenes and Sessions)
|
||||||
|
|
||||||
|
Сцены описывают многошаговые диалоги внутри плагина. Активная сцена хранится в session state, ключ которого зависит от scope, поэтому поток можно изолировать на пользователя, на чат или на пару пользователь-чат.
|
||||||
|
|
||||||
|
```go
|
||||||
|
plugin := laniakea.NewPlugin[MyDB]("signup")
|
||||||
|
|
||||||
|
plugin.NewScene("signup").
|
||||||
|
SetScope(laniakea.SceneScopeUserChat).
|
||||||
|
SetEntry("ask_name").
|
||||||
|
OnStep("ask_name", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
|
||||||
|
if ctx.Text == "" {
|
||||||
|
ctx.Answer("Как тебя зовут?")
|
||||||
|
return ctx.Stay(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.SaveData(struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{Name: ctx.Text}); err != nil {
|
||||||
|
return laniakea.SceneResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Answer("Приятно познакомиться.")
|
||||||
|
return ctx.Next("done"), nil
|
||||||
|
}).
|
||||||
|
OnStep("done", func(ctx *laniakea.SceneContext, db MyDB) (laniakea.SceneResult, error) {
|
||||||
|
return ctx.Exit(), nil
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Используйте `ctx.EnterScene("signup")`, чтобы войти в entry step, настроенный у сцены.
|
||||||
|
- Используйте `ctx.EnterSceneStep("signup", "done")`, если нужен явный стартовый step.
|
||||||
|
- Из scene handler возвращайте `ctx.Stay()`, `ctx.Next(step)`, `ctx.Exit()` или `ctx.Pass()` для управления потоком.
|
||||||
|
- `SceneActionPass` не меняет текущую session state и продолжает обычный routing бота.
|
||||||
|
- Для JSON-состояния сцены используйте `SceneContext.SaveData(...)` и `SceneContext.BindData(...)`.
|
||||||
|
- Выбирайте `SceneScopeUser`, `SceneScopeChat` или `SceneScopeUserChat` в зависимости от того, насколько широко должен разделяться диалог.
|
||||||
|
|
||||||
### tgapi: API и Uploader
|
### tgapi: API и Uploader
|
||||||
|
|
||||||
В `tgapi` есть два клиента:
|
В `tgapi` есть два клиента:
|
||||||
@@ -243,9 +280,9 @@ func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool {
|
|||||||
Этот проект лицензирован под GNU General Public License v3.0 - подробности см. в файле [LICENSE](LICENSE).
|
Этот проект лицензирован под GNU General Public License v3.0 - подробности см. в файле [LICENSE](LICENSE).
|
||||||
|
|
||||||
## 📚 Дополнительная информация
|
## 📚 Дополнительная информация
|
||||||
[GoDoc Laniakea](https://pkg.go.dev/git.nix13.pw/scuroneko/laniakea)
|
[GoDoc Laniakea](https://pkg.go.dev/git.scuroneko.dev/scuroneko/laniakea)
|
||||||
|
|
||||||
[Wiki](https://git.nix13.pw/ScuroNeko/Laniakea/wiki)
|
[Wiki](https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki)
|
||||||
|
|
||||||
[Telegram Bot API](https://core.telegram.org/bots/api)
|
[Telegram Bot API](https://core.telegram.org/bots/api)
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,20 @@ The framework backlog has moved to the wiki.
|
|||||||
|
|
||||||
Primary page:
|
Primary page:
|
||||||
|
|
||||||
- https://git.nix13.pw/ScuroNeko/Laniakea/wiki/Framework-Backlog
|
- https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki/Framework-Backlog
|
||||||
|
|
||||||
Russian page:
|
Russian page:
|
||||||
|
|
||||||
- https://git.nix13.pw/ScuroNeko/Laniakea/wiki/Framework-Backlog-RU
|
- https://git.scuroneko.dev/ScuroNeko/Laniakea/wiki/Framework-Backlog-RU
|
||||||
|
|
||||||
Current high-priority status:
|
Current priority split:
|
||||||
|
|
||||||
- `1. Conversation / Scene Model`: not implemented yet.
|
- `Priority 1`: update schema contract, user-facing vs internal error model, configuration freeze model.
|
||||||
|
- `Priority 2`: webhook runtime model, authorization and policy model, observability model.
|
||||||
|
- `Priority 3`: service layer and dependency graph model, plugin composition contract.
|
||||||
|
|
||||||
|
Completed former high-priority items:
|
||||||
|
|
||||||
|
- `1. Conversation / Scene Model`: completed in `v1.0.0-rc.12`.
|
||||||
- `2. Typed Handler Input Model`: completed in `v1.0.0-rc.12`.
|
- `2. Typed Handler Input Model`: completed in `v1.0.0-rc.12`.
|
||||||
- `3. Request Context / Cancellation Model`: completed in `v1.0.0-rc.12`.
|
- `3. Request Context / Cancellation Model`: completed in `v1.0.0-rc.12`.
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/extypes"
|
"git.scuroneko.dev/scuroneko/extypes"
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/laniakea/utils"
|
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
"github.com/alitto/pond/v2"
|
"github.com/alitto/pond/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -342,21 +342,35 @@ func (bot *Bot[T]) SetDraftProvider(p *DraftProvider) *Bot[T] {
|
|||||||
bot.draftProvider = p
|
bot.draftProvider = p
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDraftProvider returns the draft provider currently used by the bot.
|
||||||
func (bot *Bot[T]) GetDraftProvider() *DraftProvider {
|
func (bot *Bot[T]) GetDraftProvider() *DraftProvider {
|
||||||
return bot.draftProvider
|
return bot.draftProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot[T]) SetSettionStore(store SessionStore) *Bot[T] {
|
// SetSessionStore replaces the session store used for scene management.
|
||||||
|
func (bot *Bot[T]) SetSessionStore(store SessionStore) *Bot[T] {
|
||||||
|
if store == nil {
|
||||||
|
bot.logger.Warn("SetSessionStore called with nil store; using default MemorySessionStore")
|
||||||
|
return bot
|
||||||
|
}
|
||||||
bot.sessionStore = store
|
bot.sessionStore = store
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSessionStore returns the session store used for scene management.
|
||||||
func (bot *Bot[T]) GetSessionStore() SessionStore {
|
func (bot *Bot[T]) GetSessionStore() SessionStore {
|
||||||
return bot.sessionStore
|
return bot.sessionStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSceneScopePriority sets the lookup order for resolving active scene sessions.
|
||||||
func (bot *Bot[T]) SetSceneScopePriority(priority []SceneScope) *Bot[T] {
|
func (bot *Bot[T]) SetSceneScopePriority(priority []SceneScope) *Bot[T] {
|
||||||
newPriority := make([]SceneScope, 0, 3)
|
newPriority := make([]SceneScope, 0, 3)
|
||||||
for _, scope := range priority {
|
for _, scope := range priority {
|
||||||
|
if scope != SceneScopeUser && scope != SceneScopeChat && scope != SceneScopeUserChat {
|
||||||
|
bot.logger.Warnln(fmt.Sprintf("invalid scene scope %v in priority list; ignoring", scope))
|
||||||
|
continue
|
||||||
|
}
|
||||||
if slices.Index(newPriority, scope) >= 0 {
|
if slices.Index(newPriority, scope) >= 0 {
|
||||||
bot.logger.Warnln(fmt.Sprintf("duplicate scope %v in scene scope priority; ignoring duplicates", scope))
|
bot.logger.Warnln(fmt.Sprintf("duplicate scope %v in scene scope priority; ignoring duplicates", scope))
|
||||||
continue
|
continue
|
||||||
@@ -782,6 +796,7 @@ func clonePlugin[T DbContext](p *Plugin[T]) Plugin[T] {
|
|||||||
name: p.name,
|
name: p.name,
|
||||||
commands: make(map[string]*Command[T], len(p.commands)),
|
commands: make(map[string]*Command[T], len(p.commands)),
|
||||||
payloads: make(map[string]*Command[T], len(p.payloads)),
|
payloads: make(map[string]*Command[T], len(p.payloads)),
|
||||||
|
scenes: make(map[string]*Scene[T], len(p.scenes)),
|
||||||
middlewares: append(extypes.Slice[Middleware[T]](nil), p.middlewares...),
|
middlewares: append(extypes.Slice[Middleware[T]](nil), p.middlewares...),
|
||||||
skipAutoCmd: p.skipAutoCmd,
|
skipAutoCmd: p.skipAutoCmd,
|
||||||
logger: p.logger,
|
logger: p.logger,
|
||||||
@@ -795,6 +810,9 @@ func clonePlugin[T DbContext](p *Plugin[T]) Plugin[T] {
|
|||||||
for name, command := range p.payloads {
|
for name, command := range p.payloads {
|
||||||
cloned.payloads[name] = cloneCommand(command)
|
cloned.payloads[name] = cloneCommand(command)
|
||||||
}
|
}
|
||||||
|
for name, scene := range p.scenes {
|
||||||
|
cloned.scenes[name] = cloneScene(scene)
|
||||||
|
}
|
||||||
maps.Copy(cloned.handlers, p.handlers)
|
maps.Copy(cloned.handlers, p.handlers)
|
||||||
|
|
||||||
return cloned
|
return cloned
|
||||||
@@ -810,3 +828,22 @@ func cloneCommand[T DbContext](command *Command[T]) *Command[T] {
|
|||||||
cloned.middlewares = append(extypes.Slice[Middleware[T]](nil), command.middlewares...)
|
cloned.middlewares = append(extypes.Slice[Middleware[T]](nil), command.middlewares...)
|
||||||
return &cloned
|
return &cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneScene[T DbContext](scene *Scene[T]) *Scene[T] {
|
||||||
|
if scene == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := *scene
|
||||||
|
cloned.steps = make(map[string]SceneHandler[T], len(scene.steps))
|
||||||
|
cloned.commands = make(map[string]SceneHandler[T], len(scene.commands))
|
||||||
|
|
||||||
|
for name, handler := range scene.steps {
|
||||||
|
cloned.steps[name] = handler
|
||||||
|
}
|
||||||
|
for name, handler := range scene.commands {
|
||||||
|
cloned.commands[name] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BotOpts holds configuration options for initializing a Bot.
|
// BotOpts holds configuration options for initializing a Bot.
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadOptsFromEnvIgnoresEmptyUpdateTypes(t *testing.T) {
|
func TestLoadOptsFromEnvIgnoresEmptyUpdateTypes(t *testing.T) {
|
||||||
|
|||||||
+7
-9
@@ -1,17 +1,17 @@
|
|||||||
package laniakea
|
package laniakea
|
||||||
|
|
||||||
func (bot *Bot[T]) GetSession(key string) (SceneSession, error) {
|
func (bot *Bot[T]) getSession(key string) (SceneSession, error) {
|
||||||
return bot.sessionStore.Get(key)
|
return bot.sessionStore.Get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot[T]) SetSession(key string, session SceneSession) error {
|
func (bot *Bot[T]) setSession(key string, session SceneSession) error {
|
||||||
return bot.sessionStore.Set(key, session)
|
return bot.sessionStore.Set(key, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot[T]) DeleteSession(key string) error {
|
func (bot *Bot[T]) deleteSession(key string) error {
|
||||||
return bot.sessionStore.Delete(key)
|
return bot.sessionStore.Delete(key)
|
||||||
}
|
}
|
||||||
func (bot *Bot[T]) FindScene(name string) (*sceneMeta, bool) {
|
func (bot *Bot[T]) findScene(name string) (*sceneMeta, bool) {
|
||||||
for _, plugin := range bot.plugins {
|
for _, plugin := range bot.plugins {
|
||||||
scene, ok := plugin.scenes[name]
|
scene, ok := plugin.scenes[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -33,11 +33,8 @@ func (bot *Bot[T]) FindScene(name string) (*sceneMeta, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot[T]) FindSceneSession(ctx *MsgContext) (string, SceneSession, error) {
|
func (bot *Bot[T]) findSceneSession(ctx *MsgContext) (string, SceneSession, error) {
|
||||||
var zero SceneSession
|
var zero SceneSession
|
||||||
if ctx.Msg == nil {
|
|
||||||
return "", zero, ErrMessageNil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, scope := range bot.sceneScopePriority {
|
for _, scope := range bot.sceneScopePriority {
|
||||||
key, ok := buildSceneKey(scope, ctx)
|
key, ok := buildSceneKey(scope, ctx)
|
||||||
@@ -56,6 +53,7 @@ func (bot *Bot[T]) FindSceneSession(ctx *MsgContext) (string, SceneSession, erro
|
|||||||
|
|
||||||
return "", zero, ErrCantFindSession
|
return "", zero, ErrCantFindSession
|
||||||
}
|
}
|
||||||
func (bot *Bot[T]) BuildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool) {
|
|
||||||
|
func (bot *Bot[T]) buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool) {
|
||||||
return buildSceneKey(scope, ctx)
|
return buildSceneKey(scope, ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -8,8 +8,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetUpdateTypesReturnsCopy(t *testing.T) {
|
func TestGetUpdateTypesReturnsCopy(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CmdRegexp matches command names allowed for Telegram command registration.
|
// CmdRegexp matches command names allowed for Telegram command registration.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interface for generating unique draft IDs.
|
// Interface for generating unique draft IDs.
|
||||||
|
|||||||
+2
-2
@@ -5,8 +5,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDraftFlushRequiresChatID(t *testing.T) {
|
func TestDraftFlushRequiresChatID(t *testing.T) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var (
|
|||||||
ErrPayloadTypeMismatch = errors.New("payload type mismatch")
|
ErrPayloadTypeMismatch = errors.New("payload type mismatch")
|
||||||
// ErrDraftChatIDZero reports that a draft has no target chat ID.
|
// ErrDraftChatIDZero reports that a draft has no target chat ID.
|
||||||
ErrDraftChatIDZero = errors.New("zero draft chat ID")
|
ErrDraftChatIDZero = errors.New("zero draft chat ID")
|
||||||
|
// ErrMessageNil reports that a required message value is nil.
|
||||||
ErrMessageNil = errors.New("message is nil")
|
ErrMessageNil = errors.New("message is nil")
|
||||||
// ErrMessageContextNil reports that an operation requires ctx.Msg but none is set.
|
// ErrMessageContextNil reports that an operation requires ctx.Msg but none is set.
|
||||||
ErrMessageContextNil = errors.New("message context is nil")
|
ErrMessageContextNil = errors.New("message context is nil")
|
||||||
@@ -45,11 +46,18 @@ var (
|
|||||||
ErrBindArgsUnsupportedFieldType = errors.New("bind args: unsupported field type")
|
ErrBindArgsUnsupportedFieldType = errors.New("bind args: unsupported field type")
|
||||||
// ErrBindArgsConversion reports that BindArgs could not convert a string argument into a field type.
|
// ErrBindArgsConversion reports that BindArgs could not convert a string argument into a field type.
|
||||||
ErrBindArgsConversion = errors.New("bind args: conversion failed")
|
ErrBindArgsConversion = errors.New("bind args: conversion failed")
|
||||||
|
// ErrCantFindSession reports that no scene session matches the current context.
|
||||||
ErrCantFindSession = errors.New("can't find session for this context")
|
ErrCantFindSession = errors.New("can't find session for this context")
|
||||||
|
// ErrSceneNotFound reports that the requested scene is not registered.
|
||||||
ErrSceneNotFound = errors.New("scene not found")
|
ErrSceneNotFound = errors.New("scene not found")
|
||||||
|
// ErrSceneStepNotFound reports that the requested scene step is not registered.
|
||||||
ErrSceneStepNotFound = errors.New("scene step not found")
|
ErrSceneStepNotFound = errors.New("scene step not found")
|
||||||
ErrSceneCommandNotFound = errors.New("scene command not found")
|
// ErrNotInScene reports that the current context has no active scene session.
|
||||||
ErrNotInScene = errors.New("not in scene")
|
ErrNotInScene = errors.New("not in scene")
|
||||||
|
// ErrSceneEntryNotSet reports that a scene has no configured entry step.
|
||||||
|
ErrSceneEntryNotSet = errors.New("scene entry step not set")
|
||||||
|
// ErrSceneRuntimeNil reports that scene APIs were used without an attached runtime.
|
||||||
|
ErrSceneRuntimeNil = errors.New("scene runtime is nil")
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateMessageText(text string) error {
|
func validateMessageText(text string) error {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module git.nix13.pw/scuroneko/laniakea
|
module git.scuroneko.dev/scuroneko/laniakea
|
||||||
|
|
||||||
go 1.26
|
go 1.26
|
||||||
|
|
||||||
retract v1.0.0-rc.5
|
retract v1.0.0-rc.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.nix13.pw/scuroneko/extypes v1.2.2
|
git.scuroneko.dev/scuroneko/extypes v1.2.3
|
||||||
git.nix13.pw/scuroneko/slog v1.1.2
|
git.scuroneko.dev/scuroneko/slog v1.1.3
|
||||||
github.com/alitto/pond/v2 v2.7.0
|
github.com/alitto/pond/v2 v2.7.0
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
git.nix13.pw/scuroneko/extypes v1.2.2 h1:N54c1ejrPs1yfIkvYuwqI7B1+8S9mDv2GqQA6sct4dk=
|
git.scuroneko.dev/scuroneko/extypes v1.2.3 h1:n7QsfTZEn9fJNZLXGH/LkNq4cADaRk+LTu6LNMv9y6s=
|
||||||
git.nix13.pw/scuroneko/extypes v1.2.2/go.mod h1:b4XYk1OW1dVSiE2MT/OMuX/K/UItf1swytX6eroVYnk=
|
git.scuroneko.dev/scuroneko/extypes v1.2.3/go.mod h1:MhYpXC6sloLOpoM2guf64eSOrz+ET/QJZ8toobc3Ors=
|
||||||
git.nix13.pw/scuroneko/slog v1.1.2 h1:pl7tV5FN25Yso7sLYoOgBXi9+jLo5BDJHWmHlNPjpY0=
|
git.scuroneko.dev/scuroneko/slog v1.1.3 h1:vI4GZykn8gDb6OJ2xq+KLcEk38M7O4e/z1kzpeRHEHw=
|
||||||
git.nix13.pw/scuroneko/slog v1.1.2/go.mod h1:UcfRIHDqpVQHahBGM93awLDK8//AsAvOqBwwbWqMkjM=
|
git.scuroneko.dev/scuroneko/slog v1.1.3/go.mod h1:gnDap54sfZv3EuSyZd7fjOH46aLbDFpvtN2wgFcWkgE=
|
||||||
github.com/alitto/pond/v2 v2.7.0 h1:c76L+yN916m/DRXjGCeUBHHu92uWnh/g1bwVk4zyyXg=
|
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/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 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidPayloadType is returned when callback payload encoding type is unknown.
|
// ErrInvalidPayloadType is returned when callback payload encoding type is unknown.
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckPrefixesSkipsEmptyPrefixes(t *testing.T) {
|
func TestCheckPrefixesSkipsEmptyPrefixes(t *testing.T) {
|
||||||
|
|||||||
+2
-2
@@ -3,8 +3,8 @@ package laniakea
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/extypes"
|
"git.scuroneko.dev/scuroneko/extypes"
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Updates fetches new updates from Telegram API using long polling.
|
// Updates fetches new updates from Telegram API using long polling.
|
||||||
|
|||||||
+35
-12
@@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MsgContext holds the context for handling a Telegram message or callback query.
|
// MsgContext holds the context for handling a Telegram message or callback query.
|
||||||
@@ -644,26 +644,43 @@ func (ctx *MsgContext) Context() context.Context {
|
|||||||
return ctx.ctx
|
return ctx.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterScene enters the named scene at its configured entry step.
|
||||||
func (ctx *MsgContext) EnterScene(name string) error {
|
func (ctx *MsgContext) EnterScene(name string) error {
|
||||||
scene, ok := ctx.sceneRuntime.FindScene(name)
|
if ctx.sceneRuntime == nil {
|
||||||
|
return ErrSceneRuntimeNil
|
||||||
|
}
|
||||||
|
|
||||||
|
scene, ok := ctx.sceneRuntime.findScene(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrSceneNotFound
|
return ErrSceneNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
key, ok := ctx.sceneRuntime.BuildSceneKey(scene.Scope, ctx)
|
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrCantFindSession
|
return ErrCantFindSession
|
||||||
}
|
}
|
||||||
|
if scene.Entry == "" {
|
||||||
|
return ErrSceneEntryNotSet
|
||||||
|
}
|
||||||
|
if _, ok := scene.Steps[scene.Entry]; !ok {
|
||||||
|
return ErrSceneStepNotFound
|
||||||
|
}
|
||||||
|
|
||||||
session := SceneSession{
|
session := SceneSession{
|
||||||
Scene: scene.Name,
|
Scene: scene.Name,
|
||||||
Step: scene.Entry,
|
Step: scene.Entry,
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.sceneRuntime.SetSession(key, session)
|
return ctx.sceneRuntime.setSession(key, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnterSceneStep enters the named scene at a specific step.
|
||||||
func (ctx *MsgContext) EnterSceneStep(name, step string) error {
|
func (ctx *MsgContext) EnterSceneStep(name, step string) error {
|
||||||
scene, ok := ctx.sceneRuntime.FindScene(name)
|
if ctx.sceneRuntime == nil {
|
||||||
|
return ErrSceneRuntimeNil
|
||||||
|
}
|
||||||
|
|
||||||
|
scene, ok := ctx.sceneRuntime.findScene(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrSceneNotFound
|
return ErrSceneNotFound
|
||||||
}
|
}
|
||||||
@@ -671,7 +688,7 @@ func (ctx *MsgContext) EnterSceneStep(name, step string) error {
|
|||||||
return ErrSceneStepNotFound
|
return ErrSceneStepNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
key, ok := ctx.sceneRuntime.BuildSceneKey(scene.Scope, ctx)
|
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrCantFindSession
|
return ErrCantFindSession
|
||||||
}
|
}
|
||||||
@@ -681,10 +698,16 @@ func (ctx *MsgContext) EnterSceneStep(name, step string) error {
|
|||||||
Step: step,
|
Step: step,
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.sceneRuntime.SetSession(key, session)
|
return ctx.sceneRuntime.setSession(key, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExitScene leaves the currently active scene for this context.
|
||||||
func (ctx *MsgContext) ExitScene() error {
|
func (ctx *MsgContext) ExitScene() error {
|
||||||
_, session, err := ctx.sceneRuntime.FindSceneSession(ctx)
|
if ctx.sceneRuntime == nil {
|
||||||
|
return ErrSceneRuntimeNil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, session, err := ctx.sceneRuntime.findSceneSession(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -692,15 +715,15 @@ func (ctx *MsgContext) ExitScene() error {
|
|||||||
return ErrNotInScene
|
return ErrNotInScene
|
||||||
}
|
}
|
||||||
|
|
||||||
scene, ok := ctx.sceneRuntime.FindScene(session.Scene)
|
scene, ok := ctx.sceneRuntime.findScene(session.Scene)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrSceneNotFound
|
return ErrSceneNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
key, ok := ctx.sceneRuntime.BuildSceneKey(scene.Scope, ctx)
|
key, ok := ctx.sceneRuntime.buildSceneKey(scene.Scope, ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrCantFindSession
|
return ErrCantFindSession
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.sceneRuntime.DeleteSession(key)
|
return ctx.sceneRuntime.deleteSession(key)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
|
func TestAnswerPhotoIncludesDirectMessagesTopicID(t *testing.T) {
|
||||||
|
|||||||
+13
-6
@@ -4,10 +4,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/extypes"
|
"git.scuroneko.dev/scuroneko/extypes"
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/laniakea/utils"
|
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommandValueType defines the expected type of command argument.
|
// CommandValueType defines the expected type of command argument.
|
||||||
@@ -215,11 +215,18 @@ func (p *Plugin[T]) NewPayload(exec CommandExecutor[T], command string, args ...
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddScene registers a multi-step scene in the plugin.
|
||||||
func (p *Plugin[T]) AddScene(scene *Scene[T]) *Plugin[T] {
|
func (p *Plugin[T]) AddScene(scene *Scene[T]) *Plugin[T] {
|
||||||
scene.PluginName = p.name
|
if scene == nil {
|
||||||
scene.setPluginName(p.name)
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
scene.PluginName = p.name
|
||||||
|
scene.setPluginName(p.name)
|
||||||
|
p.scenes[scene.Name] = scene
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScene creates, registers, and returns a new scene owned by the plugin.
|
||||||
func (p *Plugin[T]) NewScene(name string) *Scene[T] {
|
func (p *Plugin[T]) NewScene(name string) *Scene[T] {
|
||||||
scene := NewScene[T](name)
|
scene := NewScene[T](name)
|
||||||
scene.setPluginName(p.name)
|
scene.setPluginName(p.name)
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExecRunnersRunsOnetimeSyncRunner(t *testing.T) {
|
func TestExecRunnersRunsOnetimeSyncRunner(t *testing.T) {
|
||||||
|
|||||||
@@ -5,11 +5,18 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SceneHandler handles a scene step, scene command, or fallback message.
|
||||||
type SceneHandler[T any] func(ctx *SceneContext, db T) (SceneResult, error)
|
type SceneHandler[T any] func(ctx *SceneContext, db T) (SceneResult, error)
|
||||||
|
|
||||||
|
// Scene defines a multi-step conversational flow.
|
||||||
type Scene[T any] struct {
|
type Scene[T any] struct {
|
||||||
|
// Name identifies the scene in plugin registration and session state.
|
||||||
Name string
|
Name string
|
||||||
|
// Scope controls how active scene sessions are keyed and shared.
|
||||||
Scope SceneScope
|
Scope SceneScope
|
||||||
Entry string // starting step
|
// Entry names the first step used by MsgContext.EnterScene.
|
||||||
|
Entry string
|
||||||
|
// PluginName stores the owning plugin name for scene resolution.
|
||||||
PluginName string
|
PluginName string
|
||||||
|
|
||||||
steps map[string]SceneHandler[T]
|
steps map[string]SceneHandler[T]
|
||||||
@@ -17,6 +24,7 @@ type Scene[T any] struct {
|
|||||||
message SceneHandler[T]
|
message SceneHandler[T]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewScene creates a new scene with user-chat scope by default.
|
||||||
func NewScene[T any](name string) *Scene[T] {
|
func NewScene[T any](name string) *Scene[T] {
|
||||||
return &Scene[T]{
|
return &Scene[T]{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -27,27 +35,37 @@ func NewScene[T any](name string) *Scene[T] {
|
|||||||
message: nil,
|
message: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetScope changes how scene sessions are keyed and shared.
|
||||||
func (s *Scene[T]) SetScope(scope SceneScope) *Scene[T] {
|
func (s *Scene[T]) SetScope(scope SceneScope) *Scene[T] {
|
||||||
s.Scope = scope
|
s.Scope = scope
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetEntry sets the initial step entered by MsgContext.EnterScene.
|
||||||
func (s *Scene[T]) SetEntry(step string) *Scene[T] {
|
func (s *Scene[T]) SetEntry(step string) *Scene[T] {
|
||||||
s.Entry = step
|
s.Entry = step
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scene[T]) setPluginName(name string) *Scene[T] {
|
func (s *Scene[T]) setPluginName(name string) *Scene[T] {
|
||||||
s.PluginName = name
|
s.PluginName = name
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnStep registers a handler for a named scene step.
|
||||||
func (s *Scene[T]) OnStep(step string, handler SceneHandler[T]) *Scene[T] {
|
func (s *Scene[T]) OnStep(step string, handler SceneHandler[T]) *Scene[T] {
|
||||||
s.steps[step] = handler
|
s.steps[step] = handler
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnCommand registers a command handler active while the scene is running.
|
||||||
func (s *Scene[T]) OnCommand(cmd string, handler SceneHandler[T]) *Scene[T] {
|
func (s *Scene[T]) OnCommand(cmd string, handler SceneHandler[T]) *Scene[T] {
|
||||||
s.commands[cmd] = handler
|
s.commands[cmd] = handler
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnMessage registers a fallback handler used when no scene command or step matches.
|
||||||
func (s *Scene[T]) OnMessage(handler SceneHandler[T]) *Scene[T] {
|
func (s *Scene[T]) OnMessage(handler SceneHandler[T]) *Scene[T] {
|
||||||
s.message = handler
|
s.message = handler
|
||||||
return s
|
return s
|
||||||
@@ -77,39 +95,45 @@ func (s *Scene[T]) executeMessage(ctx *SceneContext, db T) (SceneResult, bool, e
|
|||||||
return result, true, err
|
return result, true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SceneSession stores the active scene state for one session key.
|
||||||
type SceneSession struct {
|
type SceneSession struct {
|
||||||
|
// Scene is the registered scene name for the active session.
|
||||||
Scene string
|
Scene string
|
||||||
|
// Step is the current step name inside the active scene.
|
||||||
Step string
|
Step string
|
||||||
|
// Data stores opaque session payload bytes, typically JSON.
|
||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetData sets the session data. It is thread-safe and can be used to store any arbitrary data as a byte slice.
|
// SetData stores arbitrary opaque session data.
|
||||||
func (s *SceneSession) SetData(data []byte) {
|
func (s *SceneSession) SetData(data []byte) {
|
||||||
s.Data = data
|
s.Data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetData retrieves the session data. It is thread-safe and returns the data as a byte slice.
|
// GetData returns the raw session data payload.
|
||||||
func (s *SceneSession) GetData() []byte {
|
func (s *SceneSession) GetData() []byte {
|
||||||
return s.Data
|
return s.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasData reports whether the session has a non-empty data payload.
|
||||||
func (s *SceneSession) HasData() bool {
|
func (s *SceneSession) HasData() bool {
|
||||||
return len(s.Data) > 0
|
return len(s.Data) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearData clears the session data. It is thread-safe and sets the data to nil.
|
// ClearData removes any stored session data.
|
||||||
func (s *SceneSession) ClearData() {
|
func (s *SceneSession) ClearData() {
|
||||||
s.Data = nil
|
s.Data = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindData binds the session data to the provided struct.
|
// BindData unmarshals the stored JSON payload into v.
|
||||||
func (s *SceneSession) BindData(v any) error {
|
func (s *SceneSession) BindData(v any) error {
|
||||||
if len(s.Data) == 0 {
|
if len(s.Data) == 0 {
|
||||||
return nil // No data to bind, return nil error
|
return nil
|
||||||
}
|
}
|
||||||
return json.Unmarshal(s.Data, v)
|
return json.Unmarshal(s.Data, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveData saves the provided struct as JSON in the session data.
|
// SaveData marshals v as JSON and stores it in the session.
|
||||||
func (s *SceneSession) SaveData(v any) error {
|
func (s *SceneSession) SaveData(v any) error {
|
||||||
data, err := json.Marshal(v)
|
data, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -119,22 +143,27 @@ func (s *SceneSession) SaveData(v any) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionStore persists scene sessions by key.
|
||||||
type SessionStore interface {
|
type SessionStore interface {
|
||||||
Get(key string) (SceneSession, error)
|
Get(key string) (SceneSession, error)
|
||||||
Set(key string, session SceneSession) error
|
Set(key string, session SceneSession) error
|
||||||
Delete(key string) error
|
Delete(key string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MemorySessionStore stores scene sessions in memory.
|
||||||
type MemorySessionStore struct {
|
type MemorySessionStore struct {
|
||||||
store map[string]SceneSession
|
store map[string]SceneSession
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewMemorySessionStore creates an empty in-memory session store.
|
||||||
func NewMemorySessionStore() *MemorySessionStore {
|
func NewMemorySessionStore() *MemorySessionStore {
|
||||||
return &MemorySessionStore{
|
return &MemorySessionStore{
|
||||||
store: make(map[string]SceneSession),
|
store: make(map[string]SceneSession),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get returns the session stored under key, or the zero session when absent.
|
||||||
func (s *MemorySessionStore) Get(key string) (SceneSession, error) {
|
func (s *MemorySessionStore) Get(key string) (SceneSession, error) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
@@ -143,12 +172,16 @@ func (s *MemorySessionStore) Get(key string) (SceneSession, error) {
|
|||||||
}
|
}
|
||||||
return SceneSession{}, nil
|
return SceneSession{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set stores session under key.
|
||||||
func (s *MemorySessionStore) Set(key string, session SceneSession) error {
|
func (s *MemorySessionStore) Set(key string, session SceneSession) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.store[key] = session
|
s.store[key] = session
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete removes the session stored under key.
|
||||||
func (s *MemorySessionStore) Delete(key string) error {
|
func (s *MemorySessionStore) Delete(key string) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
delete(s.store, key)
|
delete(s.store, key)
|
||||||
@@ -156,35 +189,45 @@ func (s *MemorySessionStore) Delete(key string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SceneResult describes how scene execution should proceed after a handler returns.
|
||||||
type SceneResult struct {
|
type SceneResult struct {
|
||||||
Action SceneAction
|
Action SceneAction
|
||||||
Next string
|
Next string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SceneAction controls how the bot updates scene state after a handler returns.
|
||||||
type SceneAction int
|
type SceneAction int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// SceneActionStay keeps the current scene and step active.
|
||||||
SceneActionStay SceneAction = iota
|
SceneActionStay SceneAction = iota
|
||||||
|
// SceneActionNext moves the session to another named step.
|
||||||
SceneActionNext
|
SceneActionNext
|
||||||
|
// SceneActionExit removes the current scene session.
|
||||||
SceneActionExit
|
SceneActionExit
|
||||||
|
// SceneActionPass lets normal bot routing continue after the scene handler.
|
||||||
SceneActionPass
|
SceneActionPass
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SceneScope defines how scene sessions are keyed.
|
||||||
type SceneScope int
|
type SceneScope int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// SceneScopeUser shares a scene across all chats for one user.
|
||||||
SceneScopeUser SceneScope = iota
|
SceneScopeUser SceneScope = iota
|
||||||
|
// SceneScopeChat shares a scene across all users in one chat.
|
||||||
SceneScopeChat
|
SceneScopeChat
|
||||||
|
// SceneScopeUserChat isolates a scene per user-chat pair.
|
||||||
SceneScopeUserChat
|
SceneScopeUserChat
|
||||||
)
|
)
|
||||||
|
|
||||||
type sceneRuntime interface {
|
type sceneRuntime interface {
|
||||||
FindScene(name string) (*sceneMeta, bool)
|
findScene(name string) (*sceneMeta, bool)
|
||||||
GetSession(key string) (SceneSession, error)
|
getSession(key string) (SceneSession, error)
|
||||||
SetSession(key string, session SceneSession) error
|
setSession(key string, session SceneSession) error
|
||||||
DeleteSession(key string) error
|
deleteSession(key string) error
|
||||||
BuildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool)
|
buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool)
|
||||||
FindSceneSession(ctx *MsgContext) (string, SceneSession, error)
|
findSceneSession(ctx *MsgContext) (string, SceneSession, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sceneMeta struct {
|
type sceneMeta struct {
|
||||||
|
|||||||
@@ -1,23 +1,41 @@
|
|||||||
package laniakea
|
package laniakea
|
||||||
|
|
||||||
|
// SceneContext wraps MsgContext with scene session state for scene handlers.
|
||||||
type SceneContext struct {
|
type SceneContext struct {
|
||||||
*MsgContext
|
*MsgContext
|
||||||
sess SceneSession
|
sess SceneSession
|
||||||
key string
|
key string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next advances the current scene to step.
|
||||||
func (ctx *SceneContext) Next(step string) SceneResult {
|
func (ctx *SceneContext) Next(step string) SceneResult {
|
||||||
return SceneResult{
|
return SceneResult{
|
||||||
Action: SceneActionNext,
|
Action: SceneActionNext,
|
||||||
Next: step,
|
Next: step,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stay keeps the current scene step active.
|
||||||
func (ctx *SceneContext) Stay() SceneResult {
|
func (ctx *SceneContext) Stay() SceneResult {
|
||||||
return SceneResult{Action: SceneActionStay}
|
return SceneResult{Action: SceneActionStay}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exit leaves the current scene.
|
||||||
func (ctx *SceneContext) Exit() SceneResult {
|
func (ctx *SceneContext) Exit() SceneResult {
|
||||||
return SceneResult{Action: SceneActionExit}
|
return SceneResult{Action: SceneActionExit}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass stops scene handling and lets normal routing continue.
|
||||||
func (ctx *SceneContext) Pass() SceneResult {
|
func (ctx *SceneContext) Pass() SceneResult {
|
||||||
return SceneResult{Action: SceneActionPass}
|
return SceneResult{Action: SceneActionPass}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindData unmarshals the current scene session payload into v.
|
||||||
|
func (ctx *SceneContext) BindData(v any) error {
|
||||||
|
return ctx.sess.BindData(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveData marshals v and stores it in the current scene session payload.
|
||||||
|
func (ctx *SceneContext) SaveData(v any) error {
|
||||||
|
return ctx.sess.SaveData(v)
|
||||||
|
}
|
||||||
|
|||||||
+23
-6
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (bot *Bot[T]) tryHandleScene(ctx *MsgContext) (bool, error) {
|
func (bot *Bot[T]) tryHandleScene(ctx *MsgContext) (bool, error) {
|
||||||
key, session, err := bot.FindSceneSession(ctx)
|
key, session, err := bot.findSceneSession(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrCantFindSession) || errors.Is(err, ErrMessageNil) {
|
if errors.Is(err, ErrCantFindSession) || errors.Is(err, ErrMessageNil) {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -26,6 +26,9 @@ func (bot *Bot[T]) tryHandleScene(ctx *MsgContext) (bool, error) {
|
|||||||
if scene.PluginName != "" && scene.PluginName != plugin.name {
|
if scene.PluginName != "" && scene.PluginName != plugin.name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !plugin.executeMiddlewares(ctx, bot.dbContext) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
sceneCtx := &SceneContext{
|
sceneCtx := &SceneContext{
|
||||||
MsgContext: ctx,
|
MsgContext: ctx,
|
||||||
sess: session,
|
sess: session,
|
||||||
@@ -40,10 +43,15 @@ func (bot *Bot[T]) executeScene(scene *Scene[T], ctx *SceneContext) (bool, error
|
|||||||
if ctx.MsgContext == nil || ctx.sess.Scene == "" {
|
if ctx.MsgContext == nil || ctx.sess.Scene == "" {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
text := ctx.Msg.Text
|
|
||||||
|
var text string
|
||||||
|
if ctx.Msg != nil {
|
||||||
|
text = ctx.Msg.Text
|
||||||
if text == "" {
|
if text == "" {
|
||||||
text = ctx.Msg.Caption
|
text = ctx.Msg.Caption
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
prefix, cmd, args := bot.parseCommand(text)
|
prefix, cmd, args := bot.parseCommand(text)
|
||||||
if cmd != "" {
|
if cmd != "" {
|
||||||
@@ -89,7 +97,6 @@ func (bot *Bot[T]) applySceneResult(scene *Scene[T], ctx *SceneContext, result S
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
||||||
case SceneActionNext:
|
case SceneActionNext:
|
||||||
if result.Next == "" {
|
if result.Next == "" {
|
||||||
return false, ErrSceneStepNotFound
|
return false, ErrSceneStepNotFound
|
||||||
@@ -102,27 +109,37 @@ func (bot *Bot[T]) applySceneResult(scene *Scene[T], ctx *SceneContext, result S
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
||||||
case SceneActionExit:
|
case SceneActionExit:
|
||||||
if err := bot.sessionStore.Delete(ctx.key); err != nil {
|
if err := bot.sessionStore.Delete(ctx.key); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
||||||
case SceneActionPass:
|
case SceneActionPass:
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool) {
|
func buildSceneKey(scope SceneScope, ctx *MsgContext) (string, bool) {
|
||||||
|
if ctx == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
switch scope {
|
switch scope {
|
||||||
case SceneScopeUserChat:
|
case SceneScopeUserChat:
|
||||||
|
if ctx.Msg == nil || ctx.Msg.Chat == nil || ctx.FromID == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
return fmt.Sprintf("user_id:%d:chat_id:%d", ctx.FromID, ctx.Msg.Chat.ID), true
|
return fmt.Sprintf("user_id:%d:chat_id:%d", ctx.FromID, ctx.Msg.Chat.ID), true
|
||||||
case SceneScopeChat:
|
case SceneScopeChat:
|
||||||
|
if ctx.Msg == nil || ctx.Msg.Chat == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
return fmt.Sprintf("chat_id:%d", ctx.Msg.Chat.ID), true
|
return fmt.Sprintf("chat_id:%d", ctx.Msg.Chat.ID), true
|
||||||
case SceneScopeUser:
|
case SceneScopeUser:
|
||||||
|
if ctx.FromID == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
return fmt.Sprintf("user_id:%d", ctx.FromID), true
|
return fmt.Sprintf("user_id:%d", ctx.FromID), true
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
|
|||||||
+468
@@ -0,0 +1,468 @@
|
|||||||
|
package laniakea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type failingSessionStore struct {
|
||||||
|
getErr error
|
||||||
|
setErr error
|
||||||
|
deleteErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s failingSessionStore) Get(key string) (SceneSession, error) {
|
||||||
|
return SceneSession{}, s.getErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s failingSessionStore) Set(key string, session SceneSession) error {
|
||||||
|
return s.setErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s failingSessionStore) Delete(key string) error {
|
||||||
|
return s.deleteErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginAddSceneRegistersScene(t *testing.T) {
|
||||||
|
plugin := NewPlugin[NoDB]("wizard")
|
||||||
|
scene := NewScene[NoDB]("signup")
|
||||||
|
|
||||||
|
plugin.AddScene(scene)
|
||||||
|
|
||||||
|
if got, ok := plugin.scenes["signup"]; !ok || got != scene {
|
||||||
|
t.Fatalf("scene was not registered in plugin: ok=%v got=%p want=%p", ok, got, scene)
|
||||||
|
}
|
||||||
|
if scene.PluginName != "wizard" {
|
||||||
|
t.Fatalf("unexpected plugin name on scene: got %q want %q", scene.PluginName, "wizard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
|
||||||
|
plugin := NewPlugin[NoDB]("wizard")
|
||||||
|
plugin.NewScene("signup").
|
||||||
|
SetEntry("start").
|
||||||
|
OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
||||||
|
called = true
|
||||||
|
if ctx.Text != "hello there" {
|
||||||
|
t.Fatalf("unexpected scene text: got %q want %q", ctx.Text, "hello there")
|
||||||
|
}
|
||||||
|
return ctx.Exit(), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
logger: slog.CreateLogger(),
|
||||||
|
prefixes: []string{"/"},
|
||||||
|
sessionStore: NewMemorySessionStore(),
|
||||||
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
||||||
|
}
|
||||||
|
bot.AddPlugins(plugin)
|
||||||
|
|
||||||
|
sceneMeta, ok := bot.findScene("signup")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected scene metadata to be available after plugin registration")
|
||||||
|
}
|
||||||
|
if sceneMeta.Entry != "start" {
|
||||||
|
t.Fatalf("unexpected scene entry: got %q want %q", sceneMeta.Entry, "start")
|
||||||
|
}
|
||||||
|
|
||||||
|
enterCtx := &MsgContext{
|
||||||
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)}},
|
||||||
|
FromID: 42,
|
||||||
|
sceneRuntime: bot,
|
||||||
|
}
|
||||||
|
if err := enterCtx.EnterScene("signup"); err != nil {
|
||||||
|
t.Fatalf("EnterScene returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
|
UpdateID: 1,
|
||||||
|
Type: tgapi.UpdateTypeMessage,
|
||||||
|
Message: &tgapi.Message{
|
||||||
|
MessageID: 7,
|
||||||
|
Text: "hello there",
|
||||||
|
Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)},
|
||||||
|
From: &tgapi.User{ID: 42},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if !called {
|
||||||
|
t.Fatal("expected scene step handler to be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
lookupCtx := &MsgContext{
|
||||||
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)}},
|
||||||
|
FromID: 42,
|
||||||
|
}
|
||||||
|
if _, session, err := bot.findSceneSession(lookupCtx); err == nil && session.Scene != "" {
|
||||||
|
t.Fatalf("expected scene session to be removed after exit, got %#v", session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSceneKeyRejectsMissingContextFields(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
scope SceneScope
|
||||||
|
ctx *MsgContext
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil context",
|
||||||
|
scope: SceneScopeUserChat,
|
||||||
|
ctx: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing message for chat scope",
|
||||||
|
scope: SceneScopeChat,
|
||||||
|
ctx: &MsgContext{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing from id for user scope",
|
||||||
|
scope: SceneScopeUser,
|
||||||
|
ctx: &MsgContext{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing from id for user chat scope",
|
||||||
|
scope: SceneScopeUserChat,
|
||||||
|
ctx: &MsgContext{
|
||||||
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if key, ok := buildSceneKey(tt.scope, tt.ctx); ok || key != "" {
|
||||||
|
t.Fatalf("expected invalid scene key, got key=%q ok=%v", key, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
|
||||||
|
t.Run("empty entry", func(t *testing.T) {
|
||||||
|
plugin := NewPlugin[NoDB]("wizard")
|
||||||
|
plugin.NewScene("signup")
|
||||||
|
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
logger: slog.CreateLogger(),
|
||||||
|
sessionStore: NewMemorySessionStore(),
|
||||||
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
||||||
|
}
|
||||||
|
bot.AddPlugins(plugin)
|
||||||
|
|
||||||
|
ctx := &MsgContext{
|
||||||
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)}},
|
||||||
|
FromID: 42,
|
||||||
|
sceneRuntime: bot,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ctx.EnterScene("signup")
|
||||||
|
if !errors.Is(err, ErrSceneEntryNotSet) {
|
||||||
|
t.Fatalf("expected ErrSceneEntryNotSet, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing entry step", func(t *testing.T) {
|
||||||
|
plugin := NewPlugin[NoDB]("wizard")
|
||||||
|
plugin.NewScene("signup").SetEntry("start")
|
||||||
|
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
logger: slog.CreateLogger(),
|
||||||
|
sessionStore: NewMemorySessionStore(),
|
||||||
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
||||||
|
}
|
||||||
|
bot.AddPlugins(plugin)
|
||||||
|
|
||||||
|
ctx := &MsgContext{
|
||||||
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)}},
|
||||||
|
FromID: 42,
|
||||||
|
sceneRuntime: bot,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ctx.EnterScene("signup")
|
||||||
|
if !errors.Is(err, ErrSceneStepNotFound) {
|
||||||
|
t.Fatalf("expected ErrSceneStepNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSceneContextMethodsRequireRuntime(t *testing.T) {
|
||||||
|
ctx := &MsgContext{}
|
||||||
|
|
||||||
|
if err := ctx.EnterScene("signup"); !errors.Is(err, ErrSceneRuntimeNil) {
|
||||||
|
t.Fatalf("expected ErrSceneRuntimeNil from EnterScene, got %v", err)
|
||||||
|
}
|
||||||
|
if err := ctx.EnterSceneStep("signup", "start"); !errors.Is(err, ErrSceneRuntimeNil) {
|
||||||
|
t.Fatalf("expected ErrSceneRuntimeNil from EnterSceneStep, got %v", err)
|
||||||
|
}
|
||||||
|
if err := ctx.ExitScene(); !errors.Is(err, ErrSceneRuntimeNil) {
|
||||||
|
t.Fatalf("expected ErrSceneRuntimeNil from ExitScene, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
|
||||||
|
sceneCommandCalled := false
|
||||||
|
stepCalled := false
|
||||||
|
|
||||||
|
plugin := NewPlugin[NoDB]("wizard")
|
||||||
|
plugin.NewScene("signup").
|
||||||
|
SetEntry("start").
|
||||||
|
OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
||||||
|
stepCalled = true
|
||||||
|
return ctx.Stay(), nil
|
||||||
|
}).
|
||||||
|
OnCommand("cancel", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
||||||
|
sceneCommandCalled = true
|
||||||
|
if ctx.Prefix != "/" {
|
||||||
|
t.Fatalf("unexpected prefix: got %q want /", ctx.Prefix)
|
||||||
|
}
|
||||||
|
if ctx.Text != "right now" {
|
||||||
|
t.Fatalf("unexpected scene command text: got %q want %q", ctx.Text, "right now")
|
||||||
|
}
|
||||||
|
if len(ctx.Args) != 2 || ctx.Args[0] != "right" || ctx.Args[1] != "now" {
|
||||||
|
t.Fatalf("unexpected scene command args: %#v", ctx.Args)
|
||||||
|
}
|
||||||
|
return ctx.Exit(), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
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: string(tgapi.ChatTypePrivate)}},
|
||||||
|
FromID: 42,
|
||||||
|
sceneRuntime: bot,
|
||||||
|
}
|
||||||
|
if err := enterCtx.EnterScene("signup"); err != nil {
|
||||||
|
t.Fatalf("EnterScene returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
|
UpdateID: 2,
|
||||||
|
Type: tgapi.UpdateTypeMessage,
|
||||||
|
Message: &tgapi.Message{
|
||||||
|
MessageID: 8,
|
||||||
|
Text: "/cancel right now",
|
||||||
|
Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)},
|
||||||
|
From: &tgapi.User{ID: 42},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if !sceneCommandCalled {
|
||||||
|
t.Fatal("expected scene command handler to be called")
|
||||||
|
}
|
||||||
|
if stepCalled {
|
||||||
|
t.Fatal("expected scene command to short-circuit the scene step")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScenePassDoesNotPersistSessionData(t *testing.T) {
|
||||||
|
commandCalled := false
|
||||||
|
|
||||||
|
plugin := NewPlugin[NoDB]("wizard")
|
||||||
|
plugin.NewCommand(func(ctx *MsgContext, db NoDB) error {
|
||||||
|
commandCalled = true
|
||||||
|
return nil
|
||||||
|
}, "ping")
|
||||||
|
plugin.NewScene("signup").
|
||||||
|
SetEntry("start").
|
||||||
|
OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
||||||
|
if err := ctx.SaveData(struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
}{Value: "changed"}); err != nil {
|
||||||
|
t.Fatalf("SaveData returned error: %v", err)
|
||||||
|
}
|
||||||
|
return ctx.Pass(), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
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: string(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: string(tgapi.ChatTypePrivate)}},
|
||||||
|
FromID: 42,
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected scene key to be built")
|
||||||
|
}
|
||||||
|
|
||||||
|
before, err := bot.sessionStore.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get before handle returned error: %v", err)
|
||||||
|
}
|
||||||
|
if before.HasData() {
|
||||||
|
t.Fatalf("expected empty session data before handle, got %#v", before)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
|
UpdateID: 3,
|
||||||
|
Type: tgapi.UpdateTypeMessage,
|
||||||
|
Message: &tgapi.Message{
|
||||||
|
MessageID: 9,
|
||||||
|
Text: "/ping",
|
||||||
|
Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)},
|
||||||
|
From: &tgapi.User{ID: 42},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if !commandCalled {
|
||||||
|
t.Fatal("expected normal command routing to continue after SceneActionPass")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 pass: %#v", after)
|
||||||
|
}
|
||||||
|
if after.HasData() {
|
||||||
|
t.Fatalf("expected SceneActionPass to leave session data unchanged, got %#v", after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
|
||||||
|
fallbackCalled := false
|
||||||
|
|
||||||
|
plugin := NewPlugin[NoDB]("wizard")
|
||||||
|
plugin.NewScene("signup").
|
||||||
|
SetEntry("start").
|
||||||
|
OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
||||||
|
return ctx.Stay(), nil
|
||||||
|
}).
|
||||||
|
OnMessage(func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
||||||
|
fallbackCalled = true
|
||||||
|
if ctx.Text != "hello fallback" {
|
||||||
|
t.Fatalf("unexpected fallback text: got %q want %q", ctx.Text, "hello fallback")
|
||||||
|
}
|
||||||
|
return ctx.Exit(), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
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: string(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: string(tgapi.ChatTypePrivate)}},
|
||||||
|
FromID: 42,
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected scene key to be built")
|
||||||
|
}
|
||||||
|
if err := bot.sessionStore.Set(key, SceneSession{Scene: "signup", Step: "unknown"}); err != nil {
|
||||||
|
t.Fatalf("Set returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
|
UpdateID: 4,
|
||||||
|
Type: tgapi.UpdateTypeMessage,
|
||||||
|
Message: &tgapi.Message{
|
||||||
|
MessageID: 10,
|
||||||
|
Text: "hello fallback",
|
||||||
|
Chat: &tgapi.Chat{ID: 100, Type: string(tgapi.ChatTypePrivate)},
|
||||||
|
From: &tgapi.User{ID: 42},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if !fallbackCalled {
|
||||||
|
t.Fatal("expected scene fallback handler to be called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSceneSessionSupportsUserScopeWithoutMessage(t *testing.T) {
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
logger: slog.CreateLogger(),
|
||||||
|
sessionStore: NewMemorySessionStore(),
|
||||||
|
sceneScopePriority: []SceneScope{SceneScopeUser, SceneScopeChat, SceneScopeUserChat},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bot.sessionStore.Set("user_id:42", SceneSession{Scene: "signup", Step: "start"}); err != nil {
|
||||||
|
t.Fatalf("Set returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, session, err := bot.findSceneSession(&MsgContext{FromID: 42})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("findSceneSession returned error: %v", err)
|
||||||
|
}
|
||||||
|
if key != "user_id:42" {
|
||||||
|
t.Fatalf("unexpected session key: got %q want %q", key, "user_id:42")
|
||||||
|
}
|
||||||
|
if session.Scene != "signup" || session.Step != "start" {
|
||||||
|
t.Fatalf("unexpected session: %#v", session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSceneStoreErrorsPropagate(t *testing.T) {
|
||||||
|
getErr := errors.New("get failed")
|
||||||
|
setErr := errors.New("set failed")
|
||||||
|
|
||||||
|
t.Run("find scene session get error", func(t *testing.T) {
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
logger: slog.CreateLogger(),
|
||||||
|
sessionStore: failingSessionStore{getErr: getErr},
|
||||||
|
sceneScopePriority: []SceneScope{SceneScopeUser},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := bot.findSceneSession(&MsgContext{FromID: 42})
|
||||||
|
if !errors.Is(err, getErr) {
|
||||||
|
t.Fatalf("expected getErr, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("apply scene result set error", func(t *testing.T) {
|
||||||
|
scene := NewScene[NoDB]("signup").OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
||||||
|
return ctx.Stay(), nil
|
||||||
|
})
|
||||||
|
bot := &Bot[NoDB]{
|
||||||
|
logger: slog.CreateLogger(),
|
||||||
|
sessionStore: failingSessionStore{setErr: setErr},
|
||||||
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := bot.applySceneResult(scene, &SceneContext{
|
||||||
|
MsgContext: &MsgContext{},
|
||||||
|
sess: SceneSession{Scene: "signup", Step: "start"},
|
||||||
|
key: "user_id:42:chat_id:100",
|
||||||
|
}, SceneResult{Action: SceneActionStay})
|
||||||
|
if !errors.Is(err, setErr) {
|
||||||
|
t.Fatalf("expected setErr, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+2
-2
@@ -9,8 +9,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/utils"
|
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIOpts holds configuration options for initializing the Telegram API client.
|
// APIOpts holds configuration options for initializing the Telegram API client.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package tgapi
|
package tgapi
|
||||||
|
|
||||||
import "git.nix13.pw/scuroneko/extypes"
|
import "git.scuroneko.dev/scuroneko/extypes"
|
||||||
|
|
||||||
// MessageID represents a message identifier wrapper returned by some API methods.
|
// MessageID represents a message identifier wrapper returned by some API methods.
|
||||||
type MessageID struct {
|
type MessageID struct {
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/utils"
|
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateParams holds parameters for the getUpdates method.
|
// UpdateParams holds parameters for the getUpdates method.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/utils"
|
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package laniakea
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/utils"
|
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ptr returns a pointer to v.
|
// Ptr returns a pointer to v.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.nix13.pw/scuroneko/laniakea/utils"
|
"git.scuroneko.dev/scuroneko/laniakea/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type multipartEncodeParams struct {
|
type multipartEncodeParams struct {
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetLoggerLevel returns DEBUG when DEBUG=true in env, otherwise FATAL.
|
// GetLoggerLevel returns DEBUG when DEBUG=true in env, otherwise FATAL.
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.scuroneko.dev/scuroneko/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateFileLoggerWritesToConfiguredFile(t *testing.T) {
|
func TestCreateFileLoggerWritesToConfiguredFile(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user