REPOSITORY / ScuroNeko/Laniakea
Pull Requests
v1.0.0 #9
@@ -3,3 +3,4 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
test/
|
test/
|
||||||
.codex/
|
.codex/
|
||||||
|
.codex
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
- Bot configuration mutators now treat the bot as configuration-frozen after the first run begins and ignore late mutation attempts for bot-level config such as prefixes, payload defaults, plugins, middleware, runners, localization, scene session wiring, and database context injection.
|
- Bot configuration mutators now treat the bot as configuration-frozen after the first run begins and ignore late mutation attempts for bot-level config such as prefixes, payload defaults, plugins, middleware, runners, localization, scene session wiring, and database context injection.
|
||||||
- `MsgContext` godoc and field comments now describe the normalized update contract more explicitly, including when `Msg`, `From`, callback target fields, `Text`, and `Args` are expected to be populated.
|
- `MsgContext` godoc and field comments now describe the normalized update contract more explicitly, including when `Msg`, `From`, callback target fields, `Text`, and `Args` are expected to be populated.
|
||||||
- `MsgContext.Error(...)` and returned handler errors now suppress the automatic user reply when the error is explicitly marked with `AsInternalError(...)`, while keeping the previous user-visible default for unclassified errors.
|
- `MsgContext.Error(...)` and returned handler errors now suppress the automatic user reply when the error is explicitly marked with `AsInternalError(...)`, while keeping the previous user-visible default for unclassified errors.
|
||||||
|
- Godoc, README examples, and regression-test naming now consistently describe the shared generic dependency model as app data, including `NoData` and `SetAppData(...)`.
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
- Added regression coverage for the bot configuration freeze model, including ignored post-run mutations for core bot configuration methods and late registration paths.
|
- Added regression coverage for the bot configuration freeze model, including ignored post-run mutations for core bot configuration methods and late registration paths.
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It s
|
|||||||
* **Middleware Support:** Run code before or after commands (e.g., logging, access control).
|
* **Middleware Support:** Run code before or after commands (e.g., logging, access control).
|
||||||
* **Automatic Command Generation:** Generate help and command lists automatically.
|
* **Automatic Command Generation:** Generate help and command lists automatically.
|
||||||
* **Built-in Rate Limiting:** Protect your bot from hitting Telegram API limits (supports `retry_after` handling).
|
* **Built-in Rate Limiting:** Protect your bot from hitting Telegram API limits (supports `retry_after` handling).
|
||||||
* **Context-Aware:** Pass custom database or state contexts to your handlers.
|
* **Context-Aware:** Pass custom application data or state contexts to your handlers.
|
||||||
* **Fluent Interface:** Chain methods for clean configuration (e.g., `bot.ErrorTemplate(...).AddPlugins(...)`).
|
* **Configurable API:** Mix `Set...` and `Add...` helpers to configure bots clearly (for example, `bot.SetErrorTemplate(...).AddPlugins(...)`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ import (
|
|||||||
// echo is a command handler function.
|
// echo is a command handler function.
|
||||||
// It receives two parameters:
|
// It receives two parameters:
|
||||||
// - ctx: the message context (contains info about the message, sender, chat, etc.)
|
// - ctx: the message context (contains info about the message, sender, chat, etc.)
|
||||||
// - db: your custom database context (here we use NoDB, a placeholder for no database)
|
// - data: your shared application data (here we use NoData, a placeholder for no shared data)
|
||||||
func echo(ctx *laniakea.MsgContext, db laniakea.NoDB) error {
|
func echo(ctx *laniakea.MsgContext, data laniakea.NoData) error {
|
||||||
// Answer the user with the text they sent, without any command prefix.
|
// Answer the user with the text they sent, without any command prefix.
|
||||||
// ctx.Text contains the user's message with the command part stripped off.
|
// ctx.Text contains the user's message with the command part stripped off.
|
||||||
ctx.Answer(ctx.Text) // User input WITHOUT command
|
ctx.Answer(ctx.Text) // User input WITHOUT command
|
||||||
@@ -66,8 +66,8 @@ func main() {
|
|||||||
opts := &laniakea.BotOpts{Token: "TOKEN"}
|
opts := &laniakea.BotOpts{Token: "TOKEN"}
|
||||||
|
|
||||||
// 2. Initialize a new bot instance.
|
// 2. Initialize a new bot instance.
|
||||||
// We use laniakea.NoDB as the database context type (no database needed for this example).
|
// We use laniakea.NoData as the application data type (no shared data needed for this example).
|
||||||
bot, err := laniakea.NewBot[laniakea.NoDB](opts)
|
bot, err := laniakea.NewBot[laniakea.NoData](opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ func main() {
|
|||||||
|
|
||||||
// 3. Create a new plugin named "ping".
|
// 3. Create a new plugin named "ping".
|
||||||
// Plugins help group related commands and middlewares.
|
// Plugins help group related commands and middlewares.
|
||||||
p := laniakea.NewPlugin[laniakea.NoDB]("ping")
|
p := laniakea.NewPlugin[laniakea.NoData]("ping")
|
||||||
|
|
||||||
// 4. Add a command to the plugin.
|
// 4. Add a command to the plugin.
|
||||||
// p.NewCommand(echo, "echo") creates a command that triggers the 'echo' function on the "/echo" command.
|
// p.NewCommand(echo, "echo") creates a command that triggers the 'echo' function on the "/echo" command.
|
||||||
@@ -84,15 +84,15 @@ func main() {
|
|||||||
|
|
||||||
// 5. Add another command using an anonymous function (closure).
|
// 5. Add another command using an anonymous function (closure).
|
||||||
// This command simply replies "Pong" when the user sends "/ping".
|
// This command simply replies "Pong" when the user sends "/ping".
|
||||||
p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, db laniakea.NoDB) error {
|
p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, data laniakea.NoData) error {
|
||||||
ctx.Answer("Pong")
|
ctx.Answer("Pong")
|
||||||
return nil
|
return nil
|
||||||
}, "ping"))
|
}, "ping"))
|
||||||
|
|
||||||
// 6. Configure the bot with a custom error template and add the plugin.
|
// 6. Configure the bot with a custom error template and add the plugin.
|
||||||
// ErrorTemplate sets a format string for errors (where %s will be replaced by the actual error).
|
// SetErrorTemplate sets a format string for errors (where %s will be replaced by the actual error).
|
||||||
// AddPlugins(p) registers our "ping" plugin with the bot.
|
// AddPlugins(p) registers our "ping" plugin with the bot.
|
||||||
bot = bot.ErrorTemplate("Error\n\n%s").AddPlugins(p)
|
bot = bot.SetErrorTemplate("Error\n\n%s").AddPlugins(p)
|
||||||
|
|
||||||
// 7. Automatically generate commands like /start, /help, and a list of all registered commands.
|
// 7. Automatically generate commands like /start, /help, and a list of all registered commands.
|
||||||
// This is optional but very useful for most bots.
|
// This is optional but very useful for most bots.
|
||||||
@@ -109,11 +109,11 @@ func main() {
|
|||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
1. `BotOpts`: Holds configuration like the API token.
|
1. `BotOpts`: Holds configuration like the API token.
|
||||||
2. `NewBot[T]`: Creates a bot instance. The type parameter T allows you to pass a custom database context (e.g., *sql.DB) that will be available in all handlers. Use laniakea.NoDB if you don't need it.
|
2. `NewBot[T]`: Creates a bot instance. The type parameter T allows you to pass custom shared application data (for example, *sql.DB or a service container) that will be available in all handlers. Use laniakea.NoData if you don't need it.
|
||||||
3. `NewPlugin`: Creates a logical group for commands and middlewares.
|
3. `NewPlugin`: Creates a logical group for commands and middlewares.
|
||||||
4. `AddCommand`: Registers a command. The first argument is the handler function (`func(*MsgContext, T) error`), the second is the command name (without the slash).
|
4. `AddCommand`: Registers a command. The first argument is the handler function (`func(*MsgContext, T) error`), the second is the command name (without the slash).
|
||||||
5. **Handler Functions**: Receive *MsgContext (message details, methods like Answer) and your custom database context T, and return an error for centralized error handling.
|
5. **Handler Functions**: Receive *MsgContext (message details, methods like Answer) and your custom application data T, and return an error for centralized error handling.
|
||||||
6. `ErrorTemplate`: Sets a template for error messages. The %s placeholder is replaced by the actual error.
|
6. `SetErrorTemplate`: Sets a template for error messages. The %s placeholder is replaced by the actual error.
|
||||||
7. `AutoGenerateCommands`: Registers plugin-defined commands with Telegram across the supported scopes.
|
7. `AutoGenerateCommands`: Registers plugin-defined commands with Telegram across the supported scopes.
|
||||||
8. `Run()`: Starts the bot's update polling loop and returns an error if startup or polling fails.
|
8. `Run()`: Starts the bot's update polling loop and returns an error if startup or polling fails.
|
||||||
9. A `Bot` instance is single-use. After `Run()` or `RunWithContext()` returns, create a new bot instance for the next session.
|
9. A `Bot` instance is single-use. After `Run()` or `RunWithContext()` returns, create a new bot instance for the next session.
|
||||||
@@ -168,9 +168,9 @@ This split keeps method intent explicit: JSON-only calls go through `API`, file
|
|||||||
|
|
||||||
For advanced cases, `tgapi.NewRequest(...)` and `tgapi.NewUploaderRequest(...)` remain public as low-level escape hatches. They are intentionally less safe than method-specific helpers: callers must supply the correct Telegram method name and compatible request/response types themselves.
|
For advanced cases, `tgapi.NewRequest(...)` and `tgapi.NewUploaderRequest(...)` remain public as low-level escape hatches. They are intentionally less safe than method-specific helpers: callers must supply the correct Telegram method name and compatible request/response types themselves.
|
||||||
|
|
||||||
### Database Context
|
### App Data
|
||||||
|
|
||||||
The `T` in `NewBot[T]` is a powerful feature. You can pass any type, but shared dependencies such as database pools should usually use a pointer type.
|
The `T` in `NewBot[T]` is a powerful feature. You can pass any type, but shared dependencies such as database pools, service containers, or API clients should usually use a pointer type.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type MyDB struct { /* ... */ }
|
type MyDB struct { /* ... */ }
|
||||||
@@ -179,7 +179,7 @@ bot, err := laniakea.NewBot[*MyDB](opts)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
bot.DatabaseContext(db)
|
bot.SetAppData(db)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scenes and Sessions
|
### Scenes and Sessions
|
||||||
|
|||||||
+16
-16
@@ -22,8 +22,8 @@
|
|||||||
* **Поддержка промежуточных слоёв (Middleware):** Выполняйте код до или после команд (например, логирование, проверка доступа).
|
* **Поддержка промежуточных слоёв (Middleware):** Выполняйте код до или после команд (например, логирование, проверка доступа).
|
||||||
* **Автоматическая генерация команд:** Генерируйте справку и списки команд автоматически.
|
* **Автоматическая генерация команд:** Генерируйте справку и списки команд автоматически.
|
||||||
* **Встроенный ограничитель запросов (Rate Limiter):** Защитите бота от превышения лимитов Telegram API (с обработкой `retry_after`).
|
* **Встроенный ограничитель запросов (Rate Limiter):** Защитите бота от превышения лимитов Telegram API (с обработкой `retry_after`).
|
||||||
* **Контекст данных:** Передавайте свой контекст базы данных или состояния в обработчики.
|
* **Контекст данных:** Передавайте общие данные приложения или state в обработчики.
|
||||||
* **Текучий интерфейс (Fluent Interface):** Стройте цепочки методов для чистой конфигурации (например, `bot.ErrorTemplate(...).AddPlugins(...)`).
|
* **Настраиваемый API:** Комбинируйте `Set...` и `Add...` helper-методы для понятной конфигурации, например `bot.SetErrorTemplate(...).AddPlugins(...)`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ import (
|
|||||||
// echo — это функция-обработчик команды.
|
// echo — это функция-обработчик команды.
|
||||||
// Она получает два параметра:
|
// Она получает два параметра:
|
||||||
// - ctx: контекст сообщения (содержит информацию о сообщении, отправителе, чате и т.д.)
|
// - ctx: контекст сообщения (содержит информацию о сообщении, отправителе, чате и т.д.)
|
||||||
// - db: ваш пользовательский контекст базы данных (здесь мы используем NoDB — заглушку)
|
// - data: ваши общие данные приложения (здесь мы используем NoData — заглушку без общих зависимостей)
|
||||||
func echo(ctx *laniakea.MsgContext, db laniakea.NoDB) error {
|
func echo(ctx *laniakea.MsgContext, data laniakea.NoData) error {
|
||||||
// Отвечаем пользователю текстом, который он прислал, без префикса команды.
|
// Отвечаем пользователю текстом, который он прислал, без префикса команды.
|
||||||
// ctx.Text содержит сообщение пользователя, из которого удалена часть с командой.
|
// ctx.Text содержит сообщение пользователя, из которого удалена часть с командой.
|
||||||
ctx.Answer(ctx.Text) // Ввод пользователя БЕЗ команды
|
ctx.Answer(ctx.Text) // Ввод пользователя БЕЗ команды
|
||||||
@@ -67,8 +67,8 @@ func main() {
|
|||||||
opts := &laniakea.BotOpts{Token: "TOKEN"}
|
opts := &laniakea.BotOpts{Token: "TOKEN"}
|
||||||
|
|
||||||
// 2. Инициализируем новый экземпляр бота.
|
// 2. Инициализируем новый экземпляр бота.
|
||||||
// Используем laniakea.NoDB как тип контекста базы данных (база не нужна для примера).
|
// Используем laniakea.NoData как тип данных приложения (общие зависимости не нужны для примера).
|
||||||
bot, err := laniakea.NewBot[laniakea.NoDB](opts)
|
bot, err := laniakea.NewBot[laniakea.NoData](opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ func main() {
|
|||||||
|
|
||||||
// 3. Создаём новый плагин с именем "ping".
|
// 3. Создаём новый плагин с именем "ping".
|
||||||
// Плагины помогают группировать связанные команды и промежуточные обработчики.
|
// Плагины помогают группировать связанные команды и промежуточные обработчики.
|
||||||
p := laniakea.NewPlugin[laniakea.NoDB]("ping")
|
p := laniakea.NewPlugin[laniakea.NoData]("ping")
|
||||||
|
|
||||||
// 4. Добавляем команду в плагин.
|
// 4. Добавляем команду в плагин.
|
||||||
// p.NewCommand(echo, "echo") создаёт команду, которая вызывает функцию 'echo' по команде "/echo".
|
// p.NewCommand(echo, "echo") создаёт команду, которая вызывает функцию 'echo' по команде "/echo".
|
||||||
@@ -85,15 +85,15 @@ func main() {
|
|||||||
|
|
||||||
// 5. Добавляем ещё одну команду, используя анонимную функцию (замыкание).
|
// 5. Добавляем ещё одну команду, используя анонимную функцию (замыкание).
|
||||||
// Эта команда просто отвечает "Pong", когда пользователь отправляет "/ping".
|
// Эта команда просто отвечает "Pong", когда пользователь отправляет "/ping".
|
||||||
p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, db laniakea.NoDB) error {
|
p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, data laniakea.NoData) error {
|
||||||
ctx.Answer("Pong")
|
ctx.Answer("Pong")
|
||||||
return nil
|
return nil
|
||||||
}, "ping"))
|
}, "ping"))
|
||||||
|
|
||||||
// 6. Настраиваем бота: задаём шаблон ошибки и добавляем плагин.
|
// 6. Настраиваем бота: задаём шаблон ошибки и добавляем плагин.
|
||||||
// ErrorTemplate устанавливает формат для сообщений об ошибках (где %s будет заменён на текст ошибки).
|
// SetErrorTemplate устанавливает формат для сообщений об ошибках (где %s будет заменён на текст ошибки).
|
||||||
// AddPlugins(p) регистрирует наш плагин "ping" в боте.
|
// AddPlugins(p) регистрирует наш плагин "ping" в боте.
|
||||||
bot = bot.ErrorTemplate("Ошибка\n\n%s").AddPlugins(p)
|
bot = bot.SetErrorTemplate("Ошибка\n\n%s").AddPlugins(p)
|
||||||
|
|
||||||
// 7. Автоматически генерируем команды, такие как /start, /help и список всех зарегистрированных команд.
|
// 7. Автоматически генерируем команды, такие как /start, /help и список всех зарегистрированных команд.
|
||||||
// Это необязательно, но очень полезно для большинства ботов.
|
// Это необязательно, но очень полезно для большинства ботов.
|
||||||
@@ -110,11 +110,11 @@ func main() {
|
|||||||
|
|
||||||
### Как это работает
|
### Как это работает
|
||||||
1. `BotOpts`: Содержит конфигурацию, например, токен API.
|
1. `BotOpts`: Содержит конфигурацию, например, токен API.
|
||||||
2. `NewBot[T]`: Создаёт экземпляр бота. Параметр типа T позволяет передать пользовательский контекст базы данных (например, *sql.DB), который будет доступен во всех обработчиках. Используйте laniakea.NoDB, если он не нужен.
|
2. `NewBot[T]`: Создаёт экземпляр бота. Параметр типа T позволяет передать общие данные приложения (например, *sql.DB или контейнер сервисов), которые будут доступны во всех обработчиках. Используйте laniakea.NoData, если они не нужны.
|
||||||
3. `NewPlugin`: Создаёт логическую группу для команд и Middleware.
|
3. `NewPlugin`: Создаёт логическую группу для команд и Middleware.
|
||||||
4. `AddCommand`: Регистрирует команду. Первый аргумент — функция-обработчик (`func(*MsgContext, T) error`), второй — имя команды (без слеша).
|
4. `AddCommand`: Регистрирует команду. Первый аргумент — функция-обработчик (`func(*MsgContext, T) error`), второй — имя команды (без слеша).
|
||||||
5. **Функции-обработчики**: Получают *MsgContext (детали сообщения, методы типа Answer) и ваш контекст базы данных T, а ошибку возвращают для централизованной обработки.
|
5. **Функции-обработчики**: Получают *MsgContext (детали сообщения, методы типа Answer) и ваши данные приложения типа T, а ошибку возвращают для централизованной обработки.
|
||||||
6. `ErrorTemplate`: Устанавливает шаблон для сообщений об ошибках. Плейсхолдер %s заменяется на текст ошибки.
|
6. `SetErrorTemplate`: Устанавливает шаблон для сообщений об ошибках. Плейсхолдер %s заменяется на текст ошибки.
|
||||||
7. `AutoGenerateCommands`: Регистрирует команды из плагинов в Telegram для поддерживаемых scope.
|
7. `AutoGenerateCommands`: Регистрирует команды из плагинов в Telegram для поддерживаемых scope.
|
||||||
8. `Run()`: Запускает цикл опроса обновлений бота и возвращает ошибку, если старт или polling завершился неуспешно.
|
8. `Run()`: Запускает цикл опроса обновлений бота и возвращает ошибку, если старт или polling завершился неуспешно.
|
||||||
9. Экземпляр `Bot` одноразовый. После завершения `Run()` или `RunWithContext()` для следующего запуска создавайте новый бот.
|
9. Экземпляр `Bot` одноразовый. После завершения `Run()` или `RunWithContext()` для следующего запуска создавайте новый бот.
|
||||||
@@ -157,8 +157,8 @@ func myHandler(ctx *laniakea.MsgContext, db *MyDB) error {
|
|||||||
- Поля: `Text`, `Args`, `From`, `FromID`, `Msg`, `InlineMsgId`, `CallbackQueryId` и другие.
|
- Поля: `Text`, `Args`, `From`, `FromID`, `Msg`, `InlineMsgId`, `CallbackQueryId` и другие.
|
||||||
- И много других методов и полей!
|
- И много других методов и полей!
|
||||||
|
|
||||||
### Контекст базы данных (Database Context)
|
### App Data
|
||||||
Параметр типа `T` в `NewBot[T]` — мощная функция. Вы можете передать любой тип, но для разделяемых зависимостей вроде пула соединений с БД обычно стоит использовать pointer type.
|
Параметр типа `T` в `NewBot[T]` — мощная возможность. Вы можете передать любой тип, но для разделяемых зависимостей вроде пула соединений с БД, контейнера сервисов или API-клиента обычно стоит использовать pointer type.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type MyDB struct { /* ... */ }
|
type MyDB struct { /* ... */ }
|
||||||
@@ -167,7 +167,7 @@ bot, err := laniakea.NewBot[*MyDB](opts)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
bot.DatabaseContext(db)
|
bot.SetAppData(db)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Сцены и сессии (Scenes and Sessions)
|
### Сцены и сессии (Scenes and Sessions)
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ import (
|
|||||||
"github.com/alitto/pond/v2"
|
"github.com/alitto/pond/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DbContext is the generic dependency type injected into bots, plugins, and handlers.
|
// AppData is the generic shared application data type injected into bots,
|
||||||
// Use it for shared application state such as database handles or service containers.
|
// plugins, and handlers.
|
||||||
|
//
|
||||||
|
// Use it for long-lived shared dependencies such as database handles, service
|
||||||
|
// containers, API clients, or immutable configuration snapshots.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
@@ -30,18 +33,22 @@ import (
|
|||||||
// if err != nil {
|
// if err != nil {
|
||||||
// return err
|
// return err
|
||||||
// }
|
// }
|
||||||
// bot.DatabaseContext(myDB)
|
// bot.SetAppData(myDB)
|
||||||
//
|
//
|
||||||
// Use NoDB if no database is needed.
|
// Use NoData if no shared application data is needed.
|
||||||
type DbContext any
|
type AppData any
|
||||||
|
|
||||||
// NoDB is a placeholder type for bots that do not use a database.
|
// NoData is a placeholder type for bots that do not use shared application
|
||||||
// Use Bot[NoDB] to indicate no dependency injection is required.
|
// data.
|
||||||
type NoDB struct{ DbContext }
|
//
|
||||||
|
// Use Bot[NoData] to indicate no shared dependency injection is required.
|
||||||
|
type NoData struct{ AppData }
|
||||||
|
|
||||||
// DbLogger is a function type that returns a slog.LoggerWriter for database logging.
|
// AppDataLogger builds a slog.LoggerWriter from injected application data.
|
||||||
// Used to inject database-specific log output (e.g., SQL queries, ORM events).
|
//
|
||||||
type DbLogger[T DbContext] func(db T) slog.LoggerWriter
|
// Use it when shared application data exposes a log sink or adapter that should
|
||||||
|
// receive framework logs.
|
||||||
|
type AppDataLogger[T AppData] func(data T) slog.LoggerWriter
|
||||||
|
|
||||||
// BotPayloadType defines the serialization format for callback data payloads.
|
// BotPayloadType defines the serialization format for callback data payloads.
|
||||||
type BotPayloadType string
|
type BotPayloadType string
|
||||||
@@ -78,7 +85,7 @@ var (
|
|||||||
//
|
//
|
||||||
// Runtime accessors are safe for concurrent use. Configure the bot before Run.
|
// Runtime accessors are safe for concurrent use. Configure the bot before Run.
|
||||||
// A Bot is single-use: after Run or RunWithContext returns, create a new Bot for the next session.
|
// A Bot is single-use: after Run or RunWithContext returns, create a new Bot for the next session.
|
||||||
type Bot[T DbContext] struct {
|
type Bot[T AppData] struct {
|
||||||
token string
|
token string
|
||||||
debug bool
|
debug bool
|
||||||
errorTemplate string
|
errorTemplate string
|
||||||
@@ -98,11 +105,12 @@ type Bot[T DbContext] struct {
|
|||||||
|
|
||||||
api *tgapi.API // Telegram API client
|
api *tgapi.API // Telegram API client
|
||||||
uploader *tgapi.Uploader // File uploader
|
uploader *tgapi.Uploader // File uploader
|
||||||
dbContext T // Injected database context
|
l10n *L10n // Localization manager
|
||||||
hasDBContext bool
|
draftProvider *DraftProvider // Draft message builder
|
||||||
warnedValueDB bool
|
|
||||||
l10n *L10n // Localization manager
|
appData T // Injected application data
|
||||||
draftProvider *DraftProvider // Draft message builder
|
hasAppData bool
|
||||||
|
warnedValueData bool
|
||||||
|
|
||||||
sessionStore SessionStore // Session store for scene management
|
sessionStore SessionStore // Session store for scene management
|
||||||
sceneScopePriority []SceneScope
|
sceneScopePriority []SceneScope
|
||||||
@@ -328,9 +336,9 @@ func (bot *Bot[T]) GetUpdateTypes() []tgapi.UpdateType {
|
|||||||
// GetLogger returns the main bot logger.
|
// GetLogger returns the main bot logger.
|
||||||
func (bot *Bot[T]) GetLogger() *slog.Logger { return bot.logger }
|
func (bot *Bot[T]) GetLogger() *slog.Logger { return bot.logger }
|
||||||
|
|
||||||
// GetDBContext returns the injected database context.
|
// GetAppData returns the injected application data.
|
||||||
// If DatabaseContext was not called, it returns the zero value of T.
|
// If SetAppData was not called, it returns the zero value of T.
|
||||||
func (bot *Bot[T]) GetDBContext() T { return bot.dbContext }
|
func (bot *Bot[T]) GetAppData() T { return bot.appData }
|
||||||
|
|
||||||
// GetLoggerLevel returns the effective log level derived from the bot's debug
|
// GetLoggerLevel returns the effective log level derived from the bot's debug
|
||||||
// flag.
|
// flag.
|
||||||
@@ -406,27 +414,30 @@ func (bot *Bot[T]) SetSceneScopePriority(priority []SceneScope) *Bot[T] {
|
|||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseContext injects a database context into the bot.
|
// SetAppData injects shared application data into the bot.
|
||||||
// This context is accessible to plugins and middleware via GetDBContext().
|
//
|
||||||
|
// The data is accessible to commands, payload handlers, middleware, scenes,
|
||||||
|
// and runners through the generic type parameter T.
|
||||||
|
//
|
||||||
// For shared dependencies such as *sql.DB, prefer using a pointer type as T.
|
// For shared dependencies such as *sql.DB, prefer using a pointer type as T.
|
||||||
// Value-typed contexts are supported, but the bot warns once because handlers
|
// Value-typed application data is supported, but the bot warns once because
|
||||||
// receive T by value.
|
// handlers receive T by value.
|
||||||
func (bot *Bot[T]) DatabaseContext(ctx T) *Bot[T] {
|
func (bot *Bot[T]) SetAppData(ctx T) *Bot[T] {
|
||||||
if !bot.configMutable("DatabaseContext") {
|
if !bot.configMutable("SetAppData") {
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
if !bot.warnedValueDB && shouldWarnOnValueDBContext[T]() && bot.logger != nil {
|
if !bot.warnedValueData && shouldWarnOnValueAppData[T]() && bot.logger != nil {
|
||||||
bot.logger.Warnln("database context uses a value type; shared dependencies should usually use a pointer type as T")
|
bot.logger.Warnln("app data uses a value type; shared dependencies should usually use a pointer type as T")
|
||||||
bot.warnedValueDB = true
|
bot.warnedValueData = true
|
||||||
}
|
}
|
||||||
bot.dbContext = ctx
|
bot.appData = ctx
|
||||||
bot.hasDBContext = true
|
bot.hasAppData = true
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTypes sets the list of update types the bot will request from Telegram.
|
// SetUpdateTypes sets the list of update types the bot will request from Telegram.
|
||||||
// Overwrites any previously set types.
|
// Overwrites any previously set types.
|
||||||
func (bot *Bot[T]) UpdateTypes(t ...tgapi.UpdateType) *Bot[T] {
|
func (bot *Bot[T]) SetUpdateTypes(t ...tgapi.UpdateType) *Bot[T] {
|
||||||
if !bot.configMutable("UpdateTypes") {
|
if !bot.configMutable("UpdateTypes") {
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
@@ -480,10 +491,10 @@ func (bot *Bot[T]) AddPrefixes(prefixes ...string) *Bot[T] {
|
|||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorTemplate sets the format string for error messages sent to users.
|
// SetErrorTemplate sets the format string for error messages sent to users.
|
||||||
// Use "%s" to insert the error message.
|
// Use "%s" to insert the error message.
|
||||||
// Example: "❌ Error: %s" → "❌ Error: Command not found".
|
// Example: "❌ Error: %s" → "❌ Error: Command not found".
|
||||||
func (bot *Bot[T]) ErrorTemplate(s string) *Bot[T] {
|
func (bot *Bot[T]) SetErrorTemplate(s string) *Bot[T] {
|
||||||
if !bot.configMutable("ErrorTemplate") {
|
if !bot.configMutable("ErrorTemplate") {
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
@@ -491,8 +502,8 @@ func (bot *Bot[T]) ErrorTemplate(s string) *Bot[T] {
|
|||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug enables or disables debug logging.
|
// SetDebug enables or disables debug logging.
|
||||||
func (bot *Bot[T]) Debug(debug bool) *Bot[T] {
|
func (bot *Bot[T]) SetDebug(debug bool) *Bot[T] {
|
||||||
bot.debug = debug
|
bot.debug = debug
|
||||||
level := slog.FATAL
|
level := slog.FATAL
|
||||||
if debug {
|
if debug {
|
||||||
@@ -612,7 +623,7 @@ func (bot *Bot[T]) AddRunner(runner Runner[T]) *Bot[T] {
|
|||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddL10n sets the localization (i18n) provider for the bot.
|
// SetL10n sets the localization (i18n) provider for the bot.
|
||||||
//
|
//
|
||||||
// The L10n instance must be pre-populated with translations.
|
// The L10n instance must be pre-populated with translations.
|
||||||
// Translations are accessed via Bot.L10n(lang, key).
|
// Translations are accessed via Bot.L10n(lang, key).
|
||||||
@@ -622,22 +633,22 @@ func (bot *Bot[T]) AddRunner(runner Runner[T]) *Bot[T] {
|
|||||||
// l10n := l10n.New()
|
// l10n := l10n.New()
|
||||||
// l10n.Add("en", "hello", "Hello!")
|
// l10n.Add("en", "hello", "Hello!")
|
||||||
// l10n.Add("es", "hello", "¡Hola!")
|
// l10n.Add("es", "hello", "¡Hola!")
|
||||||
// bot.AddL10n(l10n)
|
// bot.SetL10n(l10n)
|
||||||
//
|
//
|
||||||
// Replaces any previously set L10n instance.
|
// Replaces any previously set L10n instance.
|
||||||
func (bot *Bot[T]) AddL10n(l *L10n) *Bot[T] {
|
func (bot *Bot[T]) SetL10n(l *L10n) *Bot[T] {
|
||||||
if !bot.configMutable("AddL10n") {
|
if !bot.configMutable("SetL10n") {
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
if l == nil {
|
if l == nil {
|
||||||
bot.logger.Warn("AddL10n called with nil L10n; localization will be disabled")
|
bot.logger.Warn("SetL10n called with nil L10n; localization will be disabled")
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
bot.l10n = l
|
bot.l10n = l
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDatabaseLoggerWriter adds a database logger writer to all loggers.
|
// AddAppDataLoggerWriter adds an app-data-backed logger writer to all loggers.
|
||||||
//
|
//
|
||||||
// The writer will receive logs from:
|
// The writer will receive logs from:
|
||||||
// - Main bot logger
|
// - Main bot logger
|
||||||
@@ -647,23 +658,23 @@ func (bot *Bot[T]) AddL10n(l *L10n) *Bot[T] {
|
|||||||
//
|
//
|
||||||
// Call this after AddPlugins if plugin loggers should also receive the writer.
|
// Call this after AddPlugins if plugin loggers should also receive the writer.
|
||||||
// Plugins registered later do not automatically inherit previously added
|
// Plugins registered later do not automatically inherit previously added
|
||||||
// database writers; call AddDatabaseLoggerWriter again after adding them.
|
// writers; call AddAppDataLoggerWriter again after adding them.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// bot.AddDatabaseLoggerWriter(func(db *MyDB) slog.LoggerWriter {
|
// bot.AddAppDataLoggerWriter(func(data *MyAppData) slog.LoggerWriter {
|
||||||
// return db.QueryLogger()
|
// return data.QueryLogger()
|
||||||
// })
|
// })
|
||||||
func (bot *Bot[T]) AddDatabaseLoggerWriter(writer DbLogger[T]) *Bot[T] {
|
func (bot *Bot[T]) AddAppDataLoggerWriter(writer AppDataLogger[T]) *Bot[T] {
|
||||||
if !bot.hasDBContext {
|
if !bot.hasAppData {
|
||||||
bot.logger.Warnln("database context is not set; skipping database logger writer")
|
bot.logger.Warnln("app data is not set; skipping app-data logger writer")
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
if isNilValue(bot.dbContext) {
|
if isNilValue(bot.appData) {
|
||||||
bot.logger.Warnln("database context is nil; skipping database logger writer")
|
bot.logger.Warnln("app data is nil; skipping app-data logger writer")
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
w := writer(bot.dbContext)
|
w := writer(bot.appData)
|
||||||
bot.logger.AddWriter(w)
|
bot.logger.AddWriter(w)
|
||||||
if bot.RequestLogger != nil {
|
if bot.RequestLogger != nil {
|
||||||
bot.RequestLogger.AddWriter(w)
|
bot.RequestLogger.AddWriter(w)
|
||||||
@@ -832,9 +843,9 @@ func isNilValue[T any](v T) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldWarnOnValueDBContext[T any]() bool {
|
func shouldWarnOnValueAppData[T any]() bool {
|
||||||
t := reflect.TypeFor[T]()
|
t := reflect.TypeFor[T]()
|
||||||
if t == reflect.TypeFor[NoDB]() {
|
if t == reflect.TypeFor[NoData]() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
@@ -845,7 +856,7 @@ func shouldWarnOnValueDBContext[T any]() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clonePlugin[T DbContext](p *Plugin[T]) Plugin[T] {
|
func clonePlugin[T AppData](p *Plugin[T]) Plugin[T] {
|
||||||
cloned := Plugin[T]{
|
cloned := 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)),
|
||||||
@@ -872,7 +883,7 @@ func clonePlugin[T DbContext](p *Plugin[T]) Plugin[T] {
|
|||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneCommand[T DbContext](command *Command[T]) *Command[T] {
|
func cloneCommand[T AppData](command *Command[T]) *Command[T] {
|
||||||
if command == nil {
|
if command == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -883,7 +894,7 @@ func cloneCommand[T DbContext](command *Command[T]) *Command[T] {
|
|||||||
return &cloned
|
return &cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneScene[T DbContext](scene *Scene[T]) *Scene[T] {
|
func cloneScene[T AppData](scene *Scene[T]) *Scene[T] {
|
||||||
if scene == nil {
|
if scene == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-51
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetUpdateTypesReturnsCopy(t *testing.T) {
|
func TestGetUpdateTypesReturnsCopy(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage}}
|
bot := &Bot[NoData]{updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage}}
|
||||||
|
|
||||||
got := bot.GetUpdateTypes()
|
got := bot.GetUpdateTypes()
|
||||||
got[0] = tgapi.UpdateTypeCallbackQuery
|
got[0] = tgapi.UpdateTypeCallbackQuery
|
||||||
@@ -24,17 +24,17 @@ func TestGetUpdateTypesReturnsCopy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddPluginsSnapshotsConfiguration(t *testing.T) {
|
func TestAddPluginsSnapshotsConfiguration(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{logger: slog.CreateLogger()}
|
bot := &Bot[NoData]{logger: slog.CreateLogger()}
|
||||||
plugin := NewPlugin[NoDB]("demo")
|
plugin := NewPlugin[NoData]("demo")
|
||||||
|
|
||||||
cmd := plugin.NewCommand(func(ctx *MsgContext, db NoDB) error { return nil }, "start")
|
cmd := plugin.NewCommand(func(ctx *MsgContext, db NoData) error { return nil }, "start")
|
||||||
plugin.AddMiddleware(NewMiddleware("base", func(ctx *MsgContext, db NoDB) bool { return true }))
|
plugin.AddMiddleware(NewMiddleware("base", func(ctx *MsgContext, db NoData) bool { return true }))
|
||||||
|
|
||||||
bot.AddPlugins(plugin)
|
bot.AddPlugins(plugin)
|
||||||
|
|
||||||
cmd.SetDescription("mutated after registration")
|
cmd.SetDescription("mutated after registration")
|
||||||
plugin.NewCommand(func(ctx *MsgContext, db NoDB) error { return nil }, "late")
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error { return nil }, "late")
|
||||||
plugin.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoDB) bool { return true }))
|
plugin.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoData) bool { return true }))
|
||||||
|
|
||||||
registered := bot.plugins[0]
|
registered := bot.plugins[0]
|
||||||
if _, exists := registered.commands["late"]; exists {
|
if _, exists := registered.commands["late"]; exists {
|
||||||
@@ -49,7 +49,7 @@ func TestAddPluginsSnapshotsConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBotPayloadTypeConfiguration(t *testing.T) {
|
func TestBotPayloadTypeConfiguration(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{payloadType: BotPayloadBase64}
|
bot := &Bot[NoData]{payloadType: BotPayloadBase64}
|
||||||
|
|
||||||
if got := bot.GetPayloadType(); got != BotPayloadBase64 {
|
if got := bot.GetPayloadType(); got != BotPayloadBase64 {
|
||||||
t.Fatalf("unexpected initial payload type: %q", got)
|
t.Fatalf("unexpected initial payload type: %q", got)
|
||||||
@@ -65,8 +65,8 @@ func TestBotPayloadTypeConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddPluginsSkipsNilPlugin(t *testing.T) {
|
func TestAddPluginsSkipsNilPlugin(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{logger: slog.CreateLogger()}
|
bot := &Bot[NoData]{logger: slog.CreateLogger()}
|
||||||
plugin := NewPlugin[NoDB]("demo")
|
plugin := NewPlugin[NoData]("demo")
|
||||||
|
|
||||||
bot.AddPlugins(nil, plugin)
|
bot.AddPlugins(nil, plugin)
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ func TestAddPluginsSkipsNilPlugin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInitLoggersFallsBackToStdoutLoggerOnFileError(t *testing.T) {
|
func TestInitLoggersFallsBackToStdoutLoggerOnFileError(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{}
|
bot := &Bot[NoData]{}
|
||||||
|
|
||||||
bot.initLoggers(&BotOpts{
|
bot.initLoggers(&BotOpts{
|
||||||
Debug: true,
|
Debug: true,
|
||||||
@@ -122,39 +122,39 @@ func TestNextPollRetryDelay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddDatabaseLoggerWriterSkipsWhenDBContextIsUnset(t *testing.T) {
|
func TestAddDatabaseLoggerWriterSkipsWhenAppDataIsUnset(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{logger: slog.CreateLogger()}
|
bot := &Bot[NoData]{logger: slog.CreateLogger()}
|
||||||
called := false
|
called := false
|
||||||
|
|
||||||
bot.AddDatabaseLoggerWriter(func(db NoDB) slog.LoggerWriter {
|
bot.AddAppDataLoggerWriter(func(db NoData) slog.LoggerWriter {
|
||||||
called = true
|
called = true
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if called {
|
if called {
|
||||||
t.Fatal("expected database logger writer to be skipped when db context is unset")
|
t.Fatal("expected app-data logger writer to be skipped when app data is unset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddDatabaseLoggerWriterSkipsWhenDBContextIsNil(t *testing.T) {
|
func TestAddDatabaseLoggerWriterSkipsWhenAppDataIsNil(t *testing.T) {
|
||||||
type testDB struct{}
|
type testDB struct{}
|
||||||
|
|
||||||
bot := &Bot[*testDB]{logger: slog.CreateLogger()}
|
bot := &Bot[*testDB]{logger: slog.CreateLogger()}
|
||||||
var db *testDB
|
var db *testDB
|
||||||
bot.DatabaseContext(db)
|
bot.SetAppData(db)
|
||||||
|
|
||||||
called := false
|
called := false
|
||||||
bot.AddDatabaseLoggerWriter(func(db *testDB) slog.LoggerWriter {
|
bot.AddAppDataLoggerWriter(func(db *testDB) slog.LoggerWriter {
|
||||||
called = true
|
called = true
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if called {
|
if called {
|
||||||
t.Fatal("expected database logger writer to be skipped when db context is nil")
|
t.Fatal("expected app-data logger writer to be skipped when app data is nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldWarnOnValueDBContext(t *testing.T) {
|
func TestShouldWarnOnValueAppData(t *testing.T) {
|
||||||
type testDB struct{}
|
type testDB struct{}
|
||||||
type dbIface interface{ Ping() error }
|
type dbIface interface{ Ping() error }
|
||||||
|
|
||||||
@@ -163,36 +163,36 @@ func TestShouldWarnOnValueDBContext(t *testing.T) {
|
|||||||
got bool
|
got bool
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{name: "NoDB", got: shouldWarnOnValueDBContext[NoDB](), want: false},
|
{name: "NoData", got: shouldWarnOnValueAppData[NoData](), want: false},
|
||||||
{name: "pointer", got: shouldWarnOnValueDBContext[*testDB](), want: false},
|
{name: "pointer", got: shouldWarnOnValueAppData[*testDB](), want: false},
|
||||||
{name: "interface", got: shouldWarnOnValueDBContext[dbIface](), want: false},
|
{name: "interface", got: shouldWarnOnValueAppData[dbIface](), want: false},
|
||||||
{name: "map", got: shouldWarnOnValueDBContext[map[string]int](), want: false},
|
{name: "map", got: shouldWarnOnValueAppData[map[string]int](), want: false},
|
||||||
{name: "struct", got: shouldWarnOnValueDBContext[testDB](), want: true},
|
{name: "struct", got: shouldWarnOnValueAppData[testDB](), want: true},
|
||||||
{name: "int", got: shouldWarnOnValueDBContext[int](), want: true},
|
{name: "int", got: shouldWarnOnValueAppData[int](), want: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if tt.got != tt.want {
|
if tt.got != tt.want {
|
||||||
t.Fatalf("shouldWarnOnValueDBContext = %v, want %v", tt.got, tt.want)
|
t.Fatalf("shouldWarnOnValueAppData = %v, want %v", tt.got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDatabaseContextMarksValueWarningOnce(t *testing.T) {
|
func TestSetAppDataMarksValueWarningOnce(t *testing.T) {
|
||||||
type testDB struct{}
|
type testDB struct{}
|
||||||
|
|
||||||
bot := &Bot[testDB]{logger: slog.CreateLogger()}
|
bot := &Bot[testDB]{logger: slog.CreateLogger()}
|
||||||
bot.DatabaseContext(testDB{})
|
bot.SetAppData(testDB{})
|
||||||
if !bot.warnedValueDB {
|
if !bot.warnedValueData {
|
||||||
t.Fatal("expected value-typed database context to mark warning state")
|
t.Fatal("expected value-typed app data to mark warning state")
|
||||||
}
|
}
|
||||||
|
|
||||||
ptrBot := &Bot[*testDB]{logger: slog.CreateLogger()}
|
ptrBot := &Bot[*testDB]{logger: slog.CreateLogger()}
|
||||||
ptrBot.DatabaseContext(&testDB{})
|
ptrBot.SetAppData(&testDB{})
|
||||||
if ptrBot.warnedValueDB {
|
if ptrBot.warnedValueData {
|
||||||
t.Fatal("did not expect pointer-typed database context to mark warning state")
|
t.Fatal("did not expect pointer-typed app data to mark warning state")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,10 +200,10 @@ func TestRunWithContextRejectsSecondRun(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
plugins: []Plugin[NoDB]{{name: "demo"}},
|
plugins: []Plugin[NoData]{{name: "demo"}},
|
||||||
updateQueue: make(chan *tgapi.Update, 1),
|
updateQueue: make(chan *tgapi.Update, 1),
|
||||||
maxWorkers: 1,
|
maxWorkers: 1,
|
||||||
}
|
}
|
||||||
@@ -235,23 +235,23 @@ func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
check func(t *testing.T, bot *Bot[*testDB])
|
check func(t *testing.T, bot *Bot[*testDB])
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "DatabaseContext",
|
name: "SetAppData",
|
||||||
check: func(t *testing.T, bot *Bot[*testDB]) {
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
||||||
original := &testDB{Name: "before"}
|
original := &testDB{Name: "before"}
|
||||||
bot.DatabaseContext(original)
|
bot.SetAppData(original)
|
||||||
if err := bot.beginRun(); err != nil {
|
if err := bot.beginRun(); err != nil {
|
||||||
t.Fatalf("beginRun returned error: %v", err)
|
t.Fatalf("beginRun returned error: %v", err)
|
||||||
}
|
}
|
||||||
t.Cleanup(bot.finishRun)
|
t.Cleanup(bot.finishRun)
|
||||||
|
|
||||||
later := &testDB{Name: "after"}
|
later := &testDB{Name: "after"}
|
||||||
bot.DatabaseContext(later)
|
bot.SetAppData(later)
|
||||||
if bot.dbContext != original {
|
if bot.appData != original {
|
||||||
t.Fatal("DatabaseContext mutated after configuration freeze")
|
t.Fatal("SetAppData mutated after configuration freeze")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -264,7 +264,7 @@ func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Cleanup(bot.finishRun)
|
t.Cleanup(bot.finishRun)
|
||||||
|
|
||||||
bot.UpdateTypes(tgapi.UpdateTypePoll)
|
bot.SetUpdateTypes(tgapi.UpdateTypePoll)
|
||||||
if !reflect.DeepEqual(bot.updateTypes, original) {
|
if !reflect.DeepEqual(bot.updateTypes, original) {
|
||||||
t.Fatalf("UpdateTypes mutated after configuration freeze: got %v want %v", bot.updateTypes, original)
|
t.Fatalf("UpdateTypes mutated after configuration freeze: got %v want %v", bot.updateTypes, original)
|
||||||
}
|
}
|
||||||
@@ -336,7 +336,7 @@ func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Cleanup(bot.finishRun)
|
t.Cleanup(bot.finishRun)
|
||||||
|
|
||||||
bot.ErrorTemplate("changed")
|
bot.SetErrorTemplate("changed")
|
||||||
if bot.errorTemplate != "%s" {
|
if bot.errorTemplate != "%s" {
|
||||||
t.Fatalf("errorTemplate mutated after configuration freeze: got %q want %q", bot.errorTemplate, "%s")
|
t.Fatalf("errorTemplate mutated after configuration freeze: got %q want %q", bot.errorTemplate, "%s")
|
||||||
}
|
}
|
||||||
@@ -396,7 +396,7 @@ func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Cleanup(bot.finishRun)
|
t.Cleanup(bot.finishRun)
|
||||||
|
|
||||||
bot.AddL10n(&L10n{})
|
bot.SetL10n(&L10n{})
|
||||||
if bot.l10n != original {
|
if bot.l10n != original {
|
||||||
t.Fatal("l10n mutated after configuration freeze")
|
t.Fatal("l10n mutated after configuration freeze")
|
||||||
}
|
}
|
||||||
@@ -412,13 +412,13 @@ func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddPluginsAndRuntimeRegistrationsNoOpAfterRunStarts(t *testing.T) {
|
func TestAddPluginsAndRuntimeRegistrationsNoOpAfterRunStarts(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
middlewares: []Middleware[NoDB]{NewMiddleware("base", func(ctx *MsgContext, db NoDB) bool { return true })},
|
middlewares: []Middleware[NoData]{NewMiddleware("base", func(ctx *MsgContext, db NoData) bool { return true })},
|
||||||
runners: []Runner[NoDB]{NewRunner("base", func(bot *Bot[NoDB]) error { return nil })},
|
runners: []Runner[NoData]{NewRunner("base", func(bot *Bot[NoData]) error { return nil })},
|
||||||
}
|
}
|
||||||
plugin := NewPlugin[NoDB]("late")
|
plugin := NewPlugin[NoData]("late")
|
||||||
|
|
||||||
if err := bot.beginRun(); err != nil {
|
if err := bot.beginRun(); err != nil {
|
||||||
t.Fatalf("beginRun returned error: %v", err)
|
t.Fatalf("beginRun returned error: %v", err)
|
||||||
@@ -426,8 +426,8 @@ func TestAddPluginsAndRuntimeRegistrationsNoOpAfterRunStarts(t *testing.T) {
|
|||||||
defer bot.finishRun()
|
defer bot.finishRun()
|
||||||
|
|
||||||
bot.AddPlugins(plugin)
|
bot.AddPlugins(plugin)
|
||||||
bot.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoDB) bool { return true }))
|
bot.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoData) bool { return true }))
|
||||||
bot.AddRunner(NewRunner("late", func(bot *Bot[NoDB]) error { return nil }))
|
bot.AddRunner(NewRunner("late", func(bot *Bot[NoData]) error { return nil }))
|
||||||
|
|
||||||
if len(bot.plugins) != 0 {
|
if len(bot.plugins) != 0 {
|
||||||
t.Fatalf("expected AddPlugins to be ignored after configuration freeze, got %d plugins", len(bot.plugins))
|
t.Fatalf("expected AddPlugins to be ignored after configuration freeze, got %d plugins", len(bot.plugins))
|
||||||
|
|||||||
@@ -43,16 +43,16 @@ func TestAutoGenerateCommandsChecksLimitBeforeDelete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
plugin := NewPlugin[NoDB]("overflow")
|
plugin := NewPlugin[NoData]("overflow")
|
||||||
exec := func(ctx *MsgContext, db NoDB) error { return nil }
|
exec := func(ctx *MsgContext, db NoData) error { return nil }
|
||||||
for i := 0; i < 101; i++ {
|
for i := 0; i < 101; i++ {
|
||||||
plugin.AddCommand(NewCommand(exec, "cmd"+strconv.Itoa(i)))
|
plugin.AddCommand(NewCommand(exec, "cmd"+strconv.Itoa(i)))
|
||||||
}
|
}
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
api: api,
|
api: api,
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
plugins: []Plugin[NoDB]{*plugin},
|
plugins: []Plugin[NoData]{*plugin},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bot.AutoGenerateCommands()
|
err := bot.AutoGenerateCommands()
|
||||||
@@ -65,8 +65,8 @@ func TestAutoGenerateCommandsChecksLimitBeforeDelete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGatherCommandsForPluginReturnsSortedCommands(t *testing.T) {
|
func TestGatherCommandsForPluginReturnsSortedCommands(t *testing.T) {
|
||||||
plugin := NewPlugin[NoDB]("sorted")
|
plugin := NewPlugin[NoData]("sorted")
|
||||||
exec := func(ctx *MsgContext, db NoDB) error { return nil }
|
exec := func(ctx *MsgContext, db NoData) error { return nil }
|
||||||
|
|
||||||
plugin.AddCommand(NewCommand(exec, "zeta"))
|
plugin.AddCommand(NewCommand(exec, "zeta"))
|
||||||
plugin.AddCommand(NewCommand(exec, "alpha"))
|
plugin.AddCommand(NewCommand(exec, "alpha"))
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ Core concepts:
|
|||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
bot, err := laniakea.NewBot[*mydb.DBContext](laniakea.LoadOptsFromEnv())
|
bot, err := laniakea.NewBot[*mydb.AppData](laniakea.LoadOptsFromEnv())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bot.DatabaseContext(myDB).
|
bot.SetAppData(myDB).
|
||||||
AddUpdateType(tgapi.UpdateTypeMessage).
|
AddUpdateType(tgapi.UpdateTypeMessage).
|
||||||
AddPrefixes("/", "!").
|
AddPrefixes("/", "!").
|
||||||
AddPlugins(&startPlugin, &helpPlugin).
|
AddPlugins(&startPlugin, &helpPlugin).
|
||||||
AddMiddleware(authMiddleware, logMiddleware).
|
AddMiddleware(authMiddleware, logMiddleware).
|
||||||
AddRunner(cleanupRunner).
|
AddRunner(cleanupRunner).
|
||||||
AddL10n(l10n.New())
|
SetL10n(l10n.New())
|
||||||
|
|
||||||
return bot.Run()
|
return bot.Run()
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -37,7 +37,7 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
|
|||||||
bot.prepareUpdateCtx(u, msgCtx)
|
bot.prepareUpdateCtx(u, msgCtx)
|
||||||
|
|
||||||
for _, middleware := range bot.middlewares {
|
for _, middleware := range bot.middlewares {
|
||||||
if !middleware.Execute(msgCtx, bot.dbContext) {
|
if !middleware.Execute(msgCtx, bot.appData) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,10 +102,10 @@ func (bot *Bot[T]) handleMessage(update *tgapi.Update, ctx *MsgContext) {
|
|||||||
if plugin.logger != nil {
|
if plugin.logger != nil {
|
||||||
ctx.Logger = plugin.logger
|
ctx.Logger = plugin.logger
|
||||||
}
|
}
|
||||||
if !plugin.executeMiddlewares(ctx, bot.dbContext) {
|
if !plugin.executeMiddlewares(ctx, bot.appData) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
plugin.executeCmd(cmd, ctx, bot.dbContext)
|
plugin.executeCmd(cmd, ctx, bot.appData)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,10 +130,10 @@ func (bot *Bot[T]) handleCallback(update *tgapi.Update, ctx *MsgContext) {
|
|||||||
if ctx.Logger == nil {
|
if ctx.Logger == nil {
|
||||||
ctx.Logger = bot.logger
|
ctx.Logger = bot.logger
|
||||||
}
|
}
|
||||||
if !plugin.executeMiddlewares(ctx, bot.dbContext) {
|
if !plugin.executeMiddlewares(ctx, bot.appData) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
plugin.executePayload(data.Command, ctx, bot.dbContext)
|
plugin.executePayload(data.Command, ctx, bot.appData)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,10 +149,10 @@ func (bot *Bot[T]) handleUpdate(u *tgapi.Update, ctx *MsgContext) {
|
|||||||
if plugin.logger != nil {
|
if plugin.logger != nil {
|
||||||
pluginCtx.Logger = plugin.logger
|
pluginCtx.Logger = plugin.logger
|
||||||
}
|
}
|
||||||
if !plugin.executeMiddlewares(pluginCtx, bot.dbContext) {
|
if !plugin.executeMiddlewares(pluginCtx, bot.appData) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := handler(pluginCtx, bot.dbContext); err != nil {
|
if err := handler(pluginCtx, bot.appData); err != nil {
|
||||||
pluginCtx.error(err)
|
pluginCtx.error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-68
@@ -13,7 +13,7 @@ func ptr[T any](v T) *T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckPrefixesSkipsEmptyPrefixes(t *testing.T) {
|
func TestCheckPrefixesSkipsEmptyPrefixes(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{prefixes: []string{"", "/"}}
|
bot := &Bot[NoData]{prefixes: []string{"", "/"}}
|
||||||
|
|
||||||
if prefix, ok := bot.checkPrefixes("hello"); ok {
|
if prefix, ok := bot.checkPrefixes("hello"); ok {
|
||||||
t.Fatalf("unexpected prefix match for plain text: %q", prefix)
|
t.Fatalf("unexpected prefix match for plain text: %q", prefix)
|
||||||
@@ -27,10 +27,10 @@ func TestBotMiddlewareReceivesLogger(t *testing.T) {
|
|||||||
logger := slog.CreateLogger()
|
logger := slog.CreateLogger()
|
||||||
called := false
|
called := false
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
middlewares: []Middleware[NoDB]{
|
middlewares: []Middleware[NoData]{
|
||||||
NewMiddleware("logger-check", func(ctx *MsgContext, db NoDB) bool {
|
NewMiddleware("logger-check", func(ctx *MsgContext, db NoData) bool {
|
||||||
called = true
|
called = true
|
||||||
if ctx.Logger != logger {
|
if ctx.Logger != logger {
|
||||||
t.Fatalf("expected bot logger in middleware context, got %#v", ctx.Logger)
|
t.Fatalf("expected bot logger in middleware context, got %#v", ctx.Logger)
|
||||||
@@ -55,8 +55,8 @@ func TestBotMiddlewareReceivesLogger(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddUpdateHandlerRejectsReservedUpdateTypes(t *testing.T) {
|
func TestAddUpdateHandlerRejectsReservedUpdateTypes(t *testing.T) {
|
||||||
plugin := NewPlugin[NoDB]("test")
|
plugin := NewPlugin[NoData]("test")
|
||||||
handler := func(ctx *MsgContext, db NoDB) error { return nil }
|
handler := func(ctx *MsgContext, db NoData) error { return nil }
|
||||||
|
|
||||||
for _, updateType := range []tgapi.UpdateType{
|
for _, updateType := range []tgapi.UpdateType{
|
||||||
tgapi.UpdateTypeMessage,
|
tgapi.UpdateTypeMessage,
|
||||||
@@ -80,14 +80,14 @@ func TestAddUpdateHandlerRejectsReservedUpdateTypes(t *testing.T) {
|
|||||||
|
|
||||||
func TestPrepareUpdateCtxContract(t *testing.T) {
|
func TestPrepareUpdateCtxContract(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
update *tgapi.Update
|
update *tgapi.Update
|
||||||
wantMsg bool
|
wantMsg bool
|
||||||
wantFrom bool
|
wantFrom bool
|
||||||
wantFromID int64
|
wantFromID int64
|
||||||
wantCallbackID string
|
wantCallbackID string
|
||||||
wantCallbackMsgID int
|
wantCallbackMsgID int
|
||||||
wantInlineMsgID string
|
wantInlineMsgID string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "message",
|
name: "message",
|
||||||
@@ -145,7 +145,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "inline query",
|
name: "inline query",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeInlineQuery,
|
Type: tgapi.UpdateTypeInlineQuery,
|
||||||
InlineQuery: &tgapi.InlineQuery{ID: "iq", From: tgapi.User{ID: 104}},
|
InlineQuery: &tgapi.InlineQuery{ID: "iq", From: tgapi.User{ID: 104}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -154,7 +154,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "chosen inline result",
|
name: "chosen inline result",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeChosenInlineResult,
|
Type: tgapi.UpdateTypeChosenInlineResult,
|
||||||
ChosenInlineResult: &tgapi.ChosenInlineResult{ResultID: "res", From: tgapi.User{ID: 105}},
|
ChosenInlineResult: &tgapi.ChosenInlineResult{ResultID: "res", From: tgapi.User{ID: 105}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -189,15 +189,15 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
InlineMessageID: ptr("inline-42"),
|
InlineMessageID: ptr("inline-42"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
wantFromID: 107,
|
wantFromID: 107,
|
||||||
wantCallbackID: "cb-2",
|
wantCallbackID: "cb-2",
|
||||||
wantInlineMsgID:"inline-42",
|
wantInlineMsgID: "inline-42",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "shipping query",
|
name: "shipping query",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeShippingQuery,
|
Type: tgapi.UpdateTypeShippingQuery,
|
||||||
ShippingQuery: &tgapi.ShippingQuery{ID: "ship", From: tgapi.User{ID: 108}},
|
ShippingQuery: &tgapi.ShippingQuery{ID: "ship", From: tgapi.User{ID: 108}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -206,7 +206,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "pre checkout query",
|
name: "pre checkout query",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypePreCheckoutQuery,
|
Type: tgapi.UpdateTypePreCheckoutQuery,
|
||||||
PreCheckoutQuery: &tgapi.PreCheckoutQuery{ID: "pre", From: tgapi.User{ID: 109}},
|
PreCheckoutQuery: &tgapi.PreCheckoutQuery{ID: "pre", From: tgapi.User{ID: 109}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -215,7 +215,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "purchased paid media",
|
name: "purchased paid media",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypePurchasedPaidMedia,
|
Type: tgapi.UpdateTypePurchasedPaidMedia,
|
||||||
PurchasedPaidMedia: &tgapi.PaidMediaPurchased{From: tgapi.User{ID: 110}},
|
PurchasedPaidMedia: &tgapi.PaidMediaPurchased{From: tgapi.User{ID: 110}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -224,7 +224,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "my chat member",
|
name: "my chat member",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeMyChatMember,
|
Type: tgapi.UpdateTypeMyChatMember,
|
||||||
MyChatMember: &tgapi.ChatMemberUpdated{From: tgapi.User{ID: 111}},
|
MyChatMember: &tgapi.ChatMemberUpdated{From: tgapi.User{ID: 111}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -233,7 +233,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "chat member",
|
name: "chat member",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeChatMember,
|
Type: tgapi.UpdateTypeChatMember,
|
||||||
ChatMember: &tgapi.ChatMemberUpdated{From: tgapi.User{ID: 112}},
|
ChatMember: &tgapi.ChatMemberUpdated{From: tgapi.User{ID: 112}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -242,7 +242,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "chat join request",
|
name: "chat join request",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeChatJoinRequest,
|
Type: tgapi.UpdateTypeChatJoinRequest,
|
||||||
ChatJoinRequest: &tgapi.ChatJoinRequest{From: tgapi.User{ID: 113}},
|
ChatJoinRequest: &tgapi.ChatJoinRequest{From: tgapi.User{ID: 113}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -251,7 +251,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "business connection",
|
name: "business connection",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeBusinessConnection,
|
Type: tgapi.UpdateTypeBusinessConnection,
|
||||||
BusinessConnection: &tgapi.BusinessConnection{User: tgapi.User{ID: 114}},
|
BusinessConnection: &tgapi.BusinessConnection{User: tgapi.User{ID: 114}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -260,7 +260,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "poll answer",
|
name: "poll answer",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypePollAnswer,
|
Type: tgapi.UpdateTypePollAnswer,
|
||||||
PollAnswer: &tgapi.PollAnswer{User: tgapi.User{ID: 115}},
|
PollAnswer: &tgapi.PollAnswer{User: tgapi.User{ID: 115}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -269,7 +269,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "message reaction",
|
name: "message reaction",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeMessageReaction,
|
Type: tgapi.UpdateTypeMessageReaction,
|
||||||
MessageReaction: &tgapi.MessageReactionUpdated{User: &tgapi.User{ID: 116}},
|
MessageReaction: &tgapi.MessageReactionUpdated{User: &tgapi.User{ID: 116}},
|
||||||
},
|
},
|
||||||
wantFrom: true,
|
wantFrom: true,
|
||||||
@@ -307,7 +307,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "message reaction count",
|
name: "message reaction count",
|
||||||
update: &tgapi.Update{
|
update: &tgapi.Update{
|
||||||
Type: tgapi.UpdateTypeMessageReactionCount,
|
Type: tgapi.UpdateTypeMessageReactionCount,
|
||||||
MessageReactionCount: &tgapi.MessageReactionCountUpdated{},
|
MessageReactionCount: &tgapi.MessageReactionCountUpdated{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -315,7 +315,7 @@ func TestPrepareUpdateCtxContract(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{}
|
bot := &Bot[NoData]{}
|
||||||
ctx := &MsgContext{}
|
ctx := &MsgContext{}
|
||||||
bot.prepareUpdateCtx(tt.update, ctx)
|
bot.prepareUpdateCtx(tt.update, ctx)
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ func TestHandleUpdateHandlersPopulateFromContext(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
called := false
|
called := false
|
||||||
plugin := NewPlugin[NoDB]("test").AddUpdateHandler(tt.update.Type, func(ctx *MsgContext, db NoDB) error {
|
plugin := NewPlugin[NoData]("test").AddUpdateHandler(tt.update.Type, func(ctx *MsgContext, db NoData) error {
|
||||||
called = true
|
called = true
|
||||||
if ctx.Update.UpdateID != tt.update.UpdateID {
|
if ctx.Update.UpdateID != tt.update.UpdateID {
|
||||||
t.Fatalf("unexpected update in context: got %d want %d", ctx.Update.UpdateID, tt.update.UpdateID)
|
t.Fatalf("unexpected update in context: got %d want %d", ctx.Update.UpdateID, tt.update.UpdateID)
|
||||||
@@ -404,9 +404,9 @@ func TestHandleUpdateHandlersPopulateFromContext(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
plugins: []Plugin[NoDB]{clonePlugin(plugin)},
|
plugins: []Plugin[NoData]{clonePlugin(plugin)},
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.handle(context.Background(), tt.update)
|
bot.handle(context.Background(), tt.update)
|
||||||
@@ -422,7 +422,7 @@ func TestHandleUpdateHandlersReceiveIsolatedContexts(t *testing.T) {
|
|||||||
firstCalled := false
|
firstCalled := false
|
||||||
secondCalled := false
|
secondCalled := false
|
||||||
|
|
||||||
first := NewPlugin[NoDB]("first").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoDB) error {
|
first := NewPlugin[NoData]("first").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoData) error {
|
||||||
firstCalled = true
|
firstCalled = true
|
||||||
if ctx.FromID != 41 {
|
if ctx.FromID != 41 {
|
||||||
t.Fatalf("unexpected FromID in first handler: got %d want 41", ctx.FromID)
|
t.Fatalf("unexpected FromID in first handler: got %d want 41", ctx.FromID)
|
||||||
@@ -433,7 +433,7 @@ func TestHandleUpdateHandlersReceiveIsolatedContexts(t *testing.T) {
|
|||||||
ctx.Args = []string{"mutated"}
|
ctx.Args = []string{"mutated"}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
second := NewPlugin[NoDB]("second").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoDB) error {
|
second := NewPlugin[NoData]("second").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoData) error {
|
||||||
secondCalled = true
|
secondCalled = true
|
||||||
if ctx.From == nil {
|
if ctx.From == nil {
|
||||||
t.Fatal("expected ctx.From to remain populated for second handler")
|
t.Fatal("expected ctx.From to remain populated for second handler")
|
||||||
@@ -450,9 +450,9 @@ func TestHandleUpdateHandlersReceiveIsolatedContexts(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
plugins: []Plugin[NoDB]{
|
plugins: []Plugin[NoData]{
|
||||||
clonePlugin(first),
|
clonePlugin(first),
|
||||||
clonePlugin(second),
|
clonePlugin(second),
|
||||||
},
|
},
|
||||||
@@ -475,8 +475,8 @@ func TestHandleUpdateHandlersReceiveIsolatedContexts(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandleChannelPostCommandWithSenderChat(t *testing.T) {
|
func TestHandleChannelPostCommandWithSenderChat(t *testing.T) {
|
||||||
called := false
|
called := false
|
||||||
plugin := NewPlugin[NoDB]("test")
|
plugin := NewPlugin[NoData]("test")
|
||||||
plugin.NewCommand(func(ctx *MsgContext, db NoDB) error {
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
|
||||||
called = true
|
called = true
|
||||||
if ctx.Msg == nil {
|
if ctx.Msg == nil {
|
||||||
t.Fatal("expected message context")
|
t.Fatal("expected message context")
|
||||||
@@ -493,10 +493,10 @@ func TestHandleChannelPostCommandWithSenderChat(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
}, "ping")
|
}, "ping")
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
plugins: []Plugin[NoDB]{clonePlugin(plugin)},
|
plugins: []Plugin[NoData]{clonePlugin(plugin)},
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.handle(context.Background(), &tgapi.Update{
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
@@ -522,18 +522,18 @@ func TestCommandHandlerBindArgsEndToEnd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var got banInput
|
var got banInput
|
||||||
plugin := NewPlugin[NoDB]("test")
|
plugin := NewPlugin[NoData]("test")
|
||||||
plugin.NewCommand(func(ctx *MsgContext, db NoDB) error {
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
|
||||||
return ctx.BindArgs(&got)
|
return ctx.BindArgs(&got)
|
||||||
}, "ban",
|
}, "ban",
|
||||||
NewCommandArg("user_id").SetValueType(CommandValueIntType).SetRequired(),
|
NewCommandArg("user_id").SetValueType(CommandValueIntType).SetRequired(),
|
||||||
NewCommandArg("reason").SetRequired(),
|
NewCommandArg("reason").SetRequired(),
|
||||||
)
|
)
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
plugins: []Plugin[NoDB]{clonePlugin(plugin)},
|
plugins: []Plugin[NoData]{clonePlugin(plugin)},
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.handle(context.Background(), &tgapi.Update{
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
@@ -559,18 +559,18 @@ func TestPayloadHandlerBindArgsEndToEnd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var got payloadInput
|
var got payloadInput
|
||||||
plugin := NewPlugin[NoDB]("test")
|
plugin := NewPlugin[NoData]("test")
|
||||||
plugin.NewPayload(func(ctx *MsgContext, db NoDB) error {
|
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
|
||||||
return ctx.BindArgs(&got)
|
return ctx.BindArgs(&got)
|
||||||
}, "approve",
|
}, "approve",
|
||||||
NewCommandArg("id").SetValueType(CommandValueIntType).SetRequired(),
|
NewCommandArg("id").SetValueType(CommandValueIntType).SetRequired(),
|
||||||
NewCommandArg("note").SetRequired(),
|
NewCommandArg("note").SetRequired(),
|
||||||
)
|
)
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
payloadType: BotPayloadJson,
|
payloadType: BotPayloadJson,
|
||||||
plugins: []Plugin[NoDB]{clonePlugin(plugin)},
|
plugins: []Plugin[NoData]{clonePlugin(plugin)},
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := encodeJsonPayload(CallbackData{
|
data, err := encodeJsonPayload(CallbackData{
|
||||||
@@ -601,12 +601,12 @@ func TestHandleEditedMessageStaysOutOfCommandFlow(t *testing.T) {
|
|||||||
commandCalled := false
|
commandCalled := false
|
||||||
updateCalled := false
|
updateCalled := false
|
||||||
|
|
||||||
plugin := NewPlugin[NoDB]("test")
|
plugin := NewPlugin[NoData]("test")
|
||||||
plugin.NewCommand(func(ctx *MsgContext, db NoDB) error {
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
|
||||||
commandCalled = true
|
commandCalled = true
|
||||||
return nil
|
return nil
|
||||||
}, "ping")
|
}, "ping")
|
||||||
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedMessage, func(ctx *MsgContext, db NoDB) error {
|
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedMessage, func(ctx *MsgContext, db NoData) error {
|
||||||
updateCalled = true
|
updateCalled = true
|
||||||
if ctx.Msg == nil {
|
if ctx.Msg == nil {
|
||||||
t.Fatal("expected ctx.Msg in edited message handler")
|
t.Fatal("expected ctx.Msg in edited message handler")
|
||||||
@@ -620,10 +620,10 @@ func TestHandleEditedMessageStaysOutOfCommandFlow(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
plugins: []Plugin[NoDB]{clonePlugin(plugin)},
|
plugins: []Plugin[NoData]{clonePlugin(plugin)},
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.handle(context.Background(), &tgapi.Update{
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
@@ -649,12 +649,12 @@ func TestHandleEditedChannelPostStaysOutOfCommandFlow(t *testing.T) {
|
|||||||
commandCalled := false
|
commandCalled := false
|
||||||
updateCalled := false
|
updateCalled := false
|
||||||
|
|
||||||
plugin := NewPlugin[NoDB]("test")
|
plugin := NewPlugin[NoData]("test")
|
||||||
plugin.NewCommand(func(ctx *MsgContext, db NoDB) error {
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
|
||||||
commandCalled = true
|
commandCalled = true
|
||||||
return nil
|
return nil
|
||||||
}, "ping")
|
}, "ping")
|
||||||
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedChannelPost, func(ctx *MsgContext, db NoDB) error {
|
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedChannelPost, func(ctx *MsgContext, db NoData) error {
|
||||||
updateCalled = true
|
updateCalled = true
|
||||||
if ctx.Msg == nil {
|
if ctx.Msg == nil {
|
||||||
t.Fatal("expected ctx.Msg in edited channel post handler")
|
t.Fatal("expected ctx.Msg in edited channel post handler")
|
||||||
@@ -662,10 +662,10 @@ func TestHandleEditedChannelPostStaysOutOfCommandFlow(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
plugins: []Plugin[NoDB]{clonePlugin(plugin)},
|
plugins: []Plugin[NoData]{clonePlugin(plugin)},
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.handle(context.Background(), &tgapi.Update{
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
@@ -688,8 +688,8 @@ func TestHandleEditedChannelPostStaysOutOfCommandFlow(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandleCallbackPopulatesMessageTargets(t *testing.T) {
|
func TestHandleCallbackPopulatesMessageTargets(t *testing.T) {
|
||||||
called := false
|
called := false
|
||||||
plugin := NewPlugin[NoDB]("test")
|
plugin := NewPlugin[NoData]("test")
|
||||||
plugin.NewPayload(func(ctx *MsgContext, db NoDB) error {
|
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
|
||||||
called = true
|
called = true
|
||||||
if ctx.CallbackQueryId != "cb-msg" {
|
if ctx.CallbackQueryId != "cb-msg" {
|
||||||
t.Fatalf("unexpected CallbackQueryId: %q", ctx.CallbackQueryId)
|
t.Fatalf("unexpected CallbackQueryId: %q", ctx.CallbackQueryId)
|
||||||
@@ -715,10 +715,10 @@ func TestHandleCallbackPopulatesMessageTargets(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
}, "approve")
|
}, "approve")
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
payloadType: BotPayloadJson,
|
payloadType: BotPayloadJson,
|
||||||
plugins: []Plugin[NoDB]{clonePlugin(plugin)},
|
plugins: []Plugin[NoData]{clonePlugin(plugin)},
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := encodeJsonPayload(CallbackData{Command: "approve", Args: []string{"7", "ok"}})
|
data, err := encodeJsonPayload(CallbackData{Command: "approve", Args: []string{"7", "ok"}})
|
||||||
@@ -747,8 +747,8 @@ func TestHandleCallbackPopulatesMessageTargets(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandleCallbackPopulatesInlineTargets(t *testing.T) {
|
func TestHandleCallbackPopulatesInlineTargets(t *testing.T) {
|
||||||
called := false
|
called := false
|
||||||
plugin := NewPlugin[NoDB]("test")
|
plugin := NewPlugin[NoData]("test")
|
||||||
plugin.NewPayload(func(ctx *MsgContext, db NoDB) error {
|
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
|
||||||
called = true
|
called = true
|
||||||
if ctx.CallbackQueryId != "cb-inline" {
|
if ctx.CallbackQueryId != "cb-inline" {
|
||||||
t.Fatalf("unexpected CallbackQueryId: %q", ctx.CallbackQueryId)
|
t.Fatalf("unexpected CallbackQueryId: %q", ctx.CallbackQueryId)
|
||||||
@@ -774,10 +774,10 @@ func TestHandleCallbackPopulatesInlineTargets(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
}, "inline.approve")
|
}, "inline.approve")
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
payloadType: BotPayloadJson,
|
payloadType: BotPayloadJson,
|
||||||
plugins: []Plugin[NoDB]{clonePlugin(plugin)},
|
plugins: []Plugin[NoData]{clonePlugin(plugin)},
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := encodeJsonPayload(CallbackData{Command: "inline.approve", Args: []string{"9"}})
|
data, err := encodeJsonPayload(CallbackData{Command: "inline.approve", Args: []string{"9"}})
|
||||||
|
|||||||
+8
-8
@@ -85,13 +85,13 @@ func (c CommandArg) SetRequired() CommandArg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CommandExecutor is the function type that executes a command.
|
// CommandExecutor is the function type that executes a command.
|
||||||
// It receives the message context and a database context (generic).
|
// It receives the message context and injected application data.
|
||||||
// Returning a non-nil error routes it through the bot's error handler.
|
// Returning a non-nil error routes it through the bot's error handler.
|
||||||
type CommandExecutor[T DbContext] func(ctx *MsgContext, dbContext T) error
|
type CommandExecutor[T AppData] func(ctx *MsgContext, dbContext T) error
|
||||||
|
|
||||||
// Command represents a bot command with arguments, description, and executor.
|
// Command represents a bot command with arguments, description, and executor.
|
||||||
// Can be registered in a Plugin and optionally skipped from auto-generation.
|
// Can be registered in a Plugin and optionally skipped from auto-generation.
|
||||||
type Command[T DbContext] struct {
|
type Command[T AppData] struct {
|
||||||
command string // The command trigger (e.g., "/start")
|
command string // The command trigger (e.g., "/start")
|
||||||
description string // Human-readable description for help
|
description string // Human-readable description for help
|
||||||
exec CommandExecutor[T] // Function to execute when command is triggered
|
exec CommandExecutor[T] // Function to execute when command is triggered
|
||||||
@@ -162,7 +162,7 @@ func (c *Command[T]) validateArgs(args []string) error {
|
|||||||
// A Plugin is intended to be fully configured before it is passed to Bot.AddPlugins.
|
// A Plugin is intended to be fully configured before it is passed to Bot.AddPlugins.
|
||||||
// After registration, treat the plugin as committed and do not mutate it further.
|
// After registration, treat the plugin as committed and do not mutate it further.
|
||||||
// Post-registration changes through the original *Plugin are not a supported API.
|
// Post-registration changes through the original *Plugin are not a supported API.
|
||||||
type Plugin[T DbContext] struct {
|
type Plugin[T AppData] struct {
|
||||||
name string // Name of the plugin (e.g., "admin", "user")
|
name string // Name of the plugin (e.g., "admin", "user")
|
||||||
commands map[string]*Command[T] // Registered commands (triggered by message)
|
commands map[string]*Command[T] // Registered commands (triggered by message)
|
||||||
payloads map[string]*Command[T] // Registered payloads (triggered by callback data)
|
payloads map[string]*Command[T] // Registered payloads (triggered by callback data)
|
||||||
@@ -177,7 +177,7 @@ type Plugin[T DbContext] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPlugin creates a new Plugin with the given name.
|
// NewPlugin creates a new Plugin with the given name.
|
||||||
func NewPlugin[T DbContext](name string) *Plugin[T] {
|
func NewPlugin[T AppData](name string) *Plugin[T] {
|
||||||
return &Plugin[T]{
|
return &Plugin[T]{
|
||||||
name: name,
|
name: name,
|
||||||
commands: make(map[string]*Command[T]),
|
commands: make(map[string]*Command[T]),
|
||||||
@@ -380,11 +380,11 @@ func (p *Plugin[T]) executeMiddlewares(ctx *MsgContext, db T) bool {
|
|||||||
// MiddlewareExecutor is the function type for middleware logic.
|
// MiddlewareExecutor is the function type for middleware logic.
|
||||||
// Returns true to continue execution, false to block it.
|
// Returns true to continue execution, false to block it.
|
||||||
// If async, return value is ignored.
|
// If async, return value is ignored.
|
||||||
type MiddlewareExecutor[T DbContext] func(ctx *MsgContext, db T) bool
|
type MiddlewareExecutor[T AppData] func(ctx *MsgContext, db T) bool
|
||||||
|
|
||||||
// Middleware represents a reusable execution interceptor.
|
// Middleware represents a reusable execution interceptor.
|
||||||
// Can be synchronous (blocking) or asynchronous (non-blocking).
|
// Can be synchronous (blocking) or asynchronous (non-blocking).
|
||||||
type Middleware[T DbContext] struct {
|
type Middleware[T AppData] struct {
|
||||||
name string // Human-readable name for logging/debugging
|
name string // Human-readable name for logging/debugging
|
||||||
executor MiddlewareExecutor[T] // Function to execute
|
executor MiddlewareExecutor[T] // Function to execute
|
||||||
order int // Optional sort order (not used yet)
|
order int // Optional sort order (not used yet)
|
||||||
@@ -392,7 +392,7 @@ type Middleware[T DbContext] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMiddleware creates a new synchronous middleware.
|
// NewMiddleware creates a new synchronous middleware.
|
||||||
func NewMiddleware[T DbContext](name string, executor MiddlewareExecutor[T]) Middleware[T] {
|
func NewMiddleware[T AppData](name string, executor MiddlewareExecutor[T]) Middleware[T] {
|
||||||
return Middleware[T]{name, executor, 0, false}
|
return Middleware[T]{name, executor, 0, false}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateArgsRequiresFullMatch(t *testing.T) {
|
func TestValidateArgsRequiresFullMatch(t *testing.T) {
|
||||||
intCmd := NewCommand[NoDB](func(ctx *MsgContext, db NoDB) error { return nil }, "int", NewCommandArg("n").SetValueType(CommandValueIntType).SetRequired())
|
intCmd := NewCommand[NoData](func(ctx *MsgContext, db NoData) error { return nil }, "int", NewCommandArg("n").SetValueType(CommandValueIntType).SetRequired())
|
||||||
if err := intCmd.validateArgs([]string{"123"}); err != nil {
|
if err := intCmd.validateArgs([]string{"123"}); err != nil {
|
||||||
t.Fatalf("expected valid integer argument, got %v", err)
|
t.Fatalf("expected valid integer argument, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ func TestValidateArgsRequiresFullMatch(t *testing.T) {
|
|||||||
t.Fatalf("expected ErrCmdArgRegexpMismatch for partial int match, got %v", err)
|
t.Fatalf("expected ErrCmdArgRegexpMismatch for partial int match, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
boolCmd := NewCommand[NoDB](func(ctx *MsgContext, db NoDB) error { return nil }, "bool", NewCommandArg("flag").SetValueType(CommandValueBoolType).SetRequired())
|
boolCmd := NewCommand[NoData](func(ctx *MsgContext, db NoData) error { return nil }, "bool", NewCommandArg("flag").SetValueType(CommandValueBoolType).SetRequired())
|
||||||
if err := boolCmd.validateArgs([]string{"false"}); err != nil {
|
if err := boolCmd.validateArgs([]string{"false"}); err != nil {
|
||||||
t.Fatalf("expected valid bool argument, got %v", err)
|
t.Fatalf("expected valid bool argument, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -24,8 +24,8 @@ func TestValidateArgsRequiresFullMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateArgsEnforcesRequiredArgIndex(t *testing.T) {
|
func TestValidateArgsEnforcesRequiredArgIndex(t *testing.T) {
|
||||||
cmd := NewCommand[NoDB](
|
cmd := NewCommand[NoData](
|
||||||
func(ctx *MsgContext, db NoDB) error { return nil },
|
func(ctx *MsgContext, db NoData) error { return nil },
|
||||||
"mixed",
|
"mixed",
|
||||||
NewCommandArg("optional"),
|
NewCommandArg("optional"),
|
||||||
NewCommandArg("required").SetRequired(),
|
NewCommandArg("required").SetRequired(),
|
||||||
|
|||||||
+3
-3
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
// RunnerFn is the function type for a runner. It receives a pointer to
|
// RunnerFn is the function type for a runner. It receives a pointer to
|
||||||
// the Bot and returns an error if execution fails.
|
// the Bot and returns an error if execution fails.
|
||||||
type RunnerFn[T DbContext] func(*Bot[T]) error
|
type RunnerFn[T AppData] func(*Bot[T]) error
|
||||||
|
|
||||||
// Runner represents a configurable background or one-time task to be
|
// Runner represents a configurable background or one-time task to be
|
||||||
// executed by a Bot.
|
// executed by a Bot.
|
||||||
@@ -20,7 +20,7 @@ type RunnerFn[T DbContext] func(*Bot[T]) error
|
|||||||
// - onetime=true, async=true: Run once in a goroutine (non-blocking).
|
// - onetime=true, async=true: Run once in a goroutine (non-blocking).
|
||||||
// - onetime=false, async=true: Run repeatedly in a goroutine with timeout.
|
// - onetime=false, async=true: Run repeatedly in a goroutine with timeout.
|
||||||
// - onetime=false, async=false: Invalid configuration — ignored with warning.
|
// - onetime=false, async=false: Invalid configuration — ignored with warning.
|
||||||
type Runner[T DbContext] struct {
|
type Runner[T AppData] struct {
|
||||||
name string // Human-readable name for logging
|
name string // Human-readable name for logging
|
||||||
onetime bool // If true, runs once; if false, runs periodically
|
onetime bool // If true, runs once; if false, runs periodically
|
||||||
async bool // If true, runs in a goroutine; else, runs synchronously
|
async bool // If true, runs in a goroutine; else, runs synchronously
|
||||||
@@ -33,7 +33,7 @@ type Runner[T DbContext] struct {
|
|||||||
//
|
//
|
||||||
// Builder methods (Onetime, Async, Timeout) can be chained to customize behavior.
|
// Builder methods (Onetime, Async, Timeout) can be chained to customize behavior.
|
||||||
// DO NOT call builder methods concurrently or after Execute().
|
// DO NOT call builder methods concurrently or after Execute().
|
||||||
func NewRunner[T DbContext](name string, fn RunnerFn[T]) Runner[T] {
|
func NewRunner[T AppData](name string, fn RunnerFn[T]) Runner[T] {
|
||||||
return Runner[T]{
|
return Runner[T]{
|
||||||
name: name,
|
name: name,
|
||||||
fn: fn,
|
fn: fn,
|
||||||
|
|||||||
+6
-6
@@ -11,10 +11,10 @@ import (
|
|||||||
|
|
||||||
func TestExecRunnersRunsOnetimeSyncRunner(t *testing.T) {
|
func TestExecRunnersRunsOnetimeSyncRunner(t *testing.T) {
|
||||||
var calls atomic.Int32
|
var calls atomic.Int32
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
runners: []Runner[NoDB]{
|
runners: []Runner[NoData]{
|
||||||
NewRunner("sync-once", func(*Bot[NoDB]) error {
|
NewRunner("sync-once", func(*Bot[NoData]) error {
|
||||||
calls.Add(1)
|
calls.Add(1)
|
||||||
return nil
|
return nil
|
||||||
}).Onetime(true).Async(false),
|
}).Onetime(true).Async(false),
|
||||||
@@ -33,10 +33,10 @@ func TestExecRunnersStopsBackgroundRunnerOnCancel(t *testing.T) {
|
|||||||
triggered := make(chan struct{}, 1)
|
triggered := make(chan struct{}, 1)
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
runners: []Runner[NoDB]{
|
runners: []Runner[NoData]{
|
||||||
NewRunner("background", func(*Bot[NoDB]) error {
|
NewRunner("background", func(*Bot[NoData]) error {
|
||||||
if calls.Add(1) == 1 {
|
if calls.Add(1) == 1 {
|
||||||
triggered <- struct{}{}
|
triggered <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -26,7 +26,7 @@ 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) {
|
if !plugin.executeMiddlewares(ctx, bot.appData) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
sceneCtx := &SceneContext{
|
sceneCtx := &SceneContext{
|
||||||
@@ -59,7 +59,7 @@ func (bot *Bot[T]) executeScene(scene *Scene[T], ctx *SceneContext) (bool, error
|
|||||||
ctx.Text = args
|
ctx.Text = args
|
||||||
ctx.Args = strings.Fields(args)
|
ctx.Args = strings.Fields(args)
|
||||||
|
|
||||||
res, matched, err := scene.executeCommand(cmd, ctx, bot.dbContext)
|
res, matched, err := scene.executeCommand(cmd, ctx, bot.appData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ func (bot *Bot[T]) executeScene(scene *Scene[T], ctx *SceneContext) (bool, error
|
|||||||
ctx.Args = nil
|
ctx.Args = nil
|
||||||
ctx.Prefix = ""
|
ctx.Prefix = ""
|
||||||
if ctx.sess.Step != "" {
|
if ctx.sess.Step != "" {
|
||||||
res, matched, err := scene.executeStep(ctx.sess.Step, ctx, bot.dbContext)
|
res, matched, err := scene.executeStep(ctx.sess.Step, ctx, bot.appData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ func (bot *Bot[T]) executeScene(scene *Scene[T], ctx *SceneContext) (bool, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res, matched, err := scene.executeMessage(ctx, bot.dbContext)
|
res, matched, err := scene.executeMessage(ctx, bot.appData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-25
@@ -28,8 +28,8 @@ func (s failingSessionStore) Delete(key string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPluginAddSceneRegistersScene(t *testing.T) {
|
func TestPluginAddSceneRegistersScene(t *testing.T) {
|
||||||
plugin := NewPlugin[NoDB]("wizard")
|
plugin := NewPlugin[NoData]("wizard")
|
||||||
scene := NewScene[NoDB]("signup")
|
scene := NewScene[NoData]("signup")
|
||||||
|
|
||||||
plugin.AddScene(scene)
|
plugin.AddScene(scene)
|
||||||
|
|
||||||
@@ -44,10 +44,10 @@ func TestPluginAddSceneRegistersScene(t *testing.T) {
|
|||||||
func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
|
func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
|
||||||
called := false
|
called := false
|
||||||
|
|
||||||
plugin := NewPlugin[NoDB]("wizard")
|
plugin := NewPlugin[NoData]("wizard")
|
||||||
plugin.NewScene("signup").
|
plugin.NewScene("signup").
|
||||||
SetEntry("start").
|
SetEntry("start").
|
||||||
OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||||
called = true
|
called = true
|
||||||
if ctx.Text != "hello there" {
|
if ctx.Text != "hello there" {
|
||||||
t.Fatalf("unexpected scene text: got %q want %q", ctx.Text, "hello there")
|
t.Fatalf("unexpected scene text: got %q want %q", ctx.Text, "hello there")
|
||||||
@@ -55,7 +55,7 @@ func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
|
|||||||
return ctx.Exit(), nil
|
return ctx.Exit(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
sessionStore: NewMemorySessionStore(),
|
sessionStore: NewMemorySessionStore(),
|
||||||
@@ -145,10 +145,10 @@ func TestBuildSceneKeyRejectsMissingContextFields(t *testing.T) {
|
|||||||
|
|
||||||
func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
|
func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
|
||||||
t.Run("empty entry", func(t *testing.T) {
|
t.Run("empty entry", func(t *testing.T) {
|
||||||
plugin := NewPlugin[NoDB]("wizard")
|
plugin := NewPlugin[NoData]("wizard")
|
||||||
plugin.NewScene("signup")
|
plugin.NewScene("signup")
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
sessionStore: NewMemorySessionStore(),
|
sessionStore: NewMemorySessionStore(),
|
||||||
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
||||||
@@ -168,10 +168,10 @@ func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("missing entry step", func(t *testing.T) {
|
t.Run("missing entry step", func(t *testing.T) {
|
||||||
plugin := NewPlugin[NoDB]("wizard")
|
plugin := NewPlugin[NoData]("wizard")
|
||||||
plugin.NewScene("signup").SetEntry("start")
|
plugin.NewScene("signup").SetEntry("start")
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
sessionStore: NewMemorySessionStore(),
|
sessionStore: NewMemorySessionStore(),
|
||||||
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
||||||
@@ -209,14 +209,14 @@ func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
|
|||||||
sceneCommandCalled := false
|
sceneCommandCalled := false
|
||||||
stepCalled := false
|
stepCalled := false
|
||||||
|
|
||||||
plugin := NewPlugin[NoDB]("wizard")
|
plugin := NewPlugin[NoData]("wizard")
|
||||||
plugin.NewScene("signup").
|
plugin.NewScene("signup").
|
||||||
SetEntry("start").
|
SetEntry("start").
|
||||||
OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||||
stepCalled = true
|
stepCalled = true
|
||||||
return ctx.Stay(), nil
|
return ctx.Stay(), nil
|
||||||
}).
|
}).
|
||||||
OnCommand("cancel", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
OnCommand("cancel", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||||
sceneCommandCalled = true
|
sceneCommandCalled = true
|
||||||
if ctx.Prefix != "/" {
|
if ctx.Prefix != "/" {
|
||||||
t.Fatalf("unexpected prefix: got %q want /", ctx.Prefix)
|
t.Fatalf("unexpected prefix: got %q want /", ctx.Prefix)
|
||||||
@@ -230,7 +230,7 @@ func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
|
|||||||
return ctx.Exit(), nil
|
return ctx.Exit(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
sessionStore: NewMemorySessionStore(),
|
sessionStore: NewMemorySessionStore(),
|
||||||
@@ -269,14 +269,14 @@ func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
|
|||||||
func TestScenePassDoesNotPersistSessionData(t *testing.T) {
|
func TestScenePassDoesNotPersistSessionData(t *testing.T) {
|
||||||
commandCalled := false
|
commandCalled := false
|
||||||
|
|
||||||
plugin := NewPlugin[NoDB]("wizard")
|
plugin := NewPlugin[NoData]("wizard")
|
||||||
plugin.NewCommand(func(ctx *MsgContext, db NoDB) error {
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
|
||||||
commandCalled = true
|
commandCalled = true
|
||||||
return nil
|
return nil
|
||||||
}, "ping")
|
}, "ping")
|
||||||
plugin.NewScene("signup").
|
plugin.NewScene("signup").
|
||||||
SetEntry("start").
|
SetEntry("start").
|
||||||
OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||||
if err := ctx.SaveData(struct {
|
if err := ctx.SaveData(struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}{Value: "changed"}); err != nil {
|
}{Value: "changed"}); err != nil {
|
||||||
@@ -285,7 +285,7 @@ func TestScenePassDoesNotPersistSessionData(t *testing.T) {
|
|||||||
return ctx.Pass(), nil
|
return ctx.Pass(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
sessionStore: NewMemorySessionStore(),
|
sessionStore: NewMemorySessionStore(),
|
||||||
@@ -348,13 +348,13 @@ func TestScenePassDoesNotPersistSessionData(t *testing.T) {
|
|||||||
func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
|
func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
|
||||||
fallbackCalled := false
|
fallbackCalled := false
|
||||||
|
|
||||||
plugin := NewPlugin[NoDB]("wizard")
|
plugin := NewPlugin[NoData]("wizard")
|
||||||
plugin.NewScene("signup").
|
plugin.NewScene("signup").
|
||||||
SetEntry("start").
|
SetEntry("start").
|
||||||
OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||||
return ctx.Stay(), nil
|
return ctx.Stay(), nil
|
||||||
}).
|
}).
|
||||||
OnMessage(func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
OnMessage(func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||||
fallbackCalled = true
|
fallbackCalled = true
|
||||||
if ctx.Text != "hello fallback" {
|
if ctx.Text != "hello fallback" {
|
||||||
t.Fatalf("unexpected fallback text: got %q want %q", ctx.Text, "hello fallback")
|
t.Fatalf("unexpected fallback text: got %q want %q", ctx.Text, "hello fallback")
|
||||||
@@ -362,7 +362,7 @@ func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
|
|||||||
return ctx.Exit(), nil
|
return ctx.Exit(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
prefixes: []string{"/"},
|
prefixes: []string{"/"},
|
||||||
sessionStore: NewMemorySessionStore(),
|
sessionStore: NewMemorySessionStore(),
|
||||||
@@ -407,7 +407,7 @@ func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFindSceneSessionSupportsUserScopeWithoutMessage(t *testing.T) {
|
func TestFindSceneSessionSupportsUserScopeWithoutMessage(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
sessionStore: NewMemorySessionStore(),
|
sessionStore: NewMemorySessionStore(),
|
||||||
sceneScopePriority: []SceneScope{SceneScopeUser, SceneScopeChat, SceneScopeUserChat},
|
sceneScopePriority: []SceneScope{SceneScopeUser, SceneScopeChat, SceneScopeUserChat},
|
||||||
@@ -434,7 +434,7 @@ func TestSceneStoreErrorsPropagate(t *testing.T) {
|
|||||||
setErr := errors.New("set failed")
|
setErr := errors.New("set failed")
|
||||||
|
|
||||||
t.Run("find scene session get error", func(t *testing.T) {
|
t.Run("find scene session get error", func(t *testing.T) {
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
sessionStore: failingSessionStore{getErr: getErr},
|
sessionStore: failingSessionStore{getErr: getErr},
|
||||||
sceneScopePriority: []SceneScope{SceneScopeUser},
|
sceneScopePriority: []SceneScope{SceneScopeUser},
|
||||||
@@ -447,10 +447,10 @@ func TestSceneStoreErrorsPropagate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("apply scene result set error", func(t *testing.T) {
|
t.Run("apply scene result set error", func(t *testing.T) {
|
||||||
scene := NewScene[NoDB]("signup").OnStep("start", func(ctx *SceneContext, db NoDB) (SceneResult, error) {
|
scene := NewScene[NoData]("signup").OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||||
return ctx.Stay(), nil
|
return ctx.Stay(), nil
|
||||||
})
|
})
|
||||||
bot := &Bot[NoDB]{
|
bot := &Bot[NoData]{
|
||||||
logger: slog.CreateLogger(),
|
logger: slog.CreateLogger(),
|
||||||
sessionStore: failingSessionStore{setErr: setErr},
|
sessionStore: failingSessionStore{setErr: setErr},
|
||||||
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
||||||
|
|||||||
Reference in New Issue
Block a user