diff --git a/README.md b/README.md index 7e8892d..5af984b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,204 @@ # Laniakea -A lightweight, easy to use and performance Telegram API wrapper for bot development. \ No newline at end of file +[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&style=flat-square)](https://go.dev/) +[![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg?style=flat-square)](LICENSE) +![Gitea Release](https://img.shields.io/gitea/v/release/ScuroNeko/Laniakea?gitea_url=https%3A%2F%2Fgit.nix13.pw&sort=semver&display_name=release&style=flat-square&color=purple&link=https%3A%2F%2Fgit.nix13.pw%2FScuroNeko%2FLaniakea%2Freleases) + +A lightweight, easy-to-use, and performant Telegram Bot API wrapper for Go. It simplifies bot development with a clean plugin system, middleware support, automatic command generation, and built-in rate limiting. + +[На русском](README_RU.md) + +--- + +## ✨ Features +* **Simple & Intuitive API:** Designed for ease of use, based on practical examples. +* **Plugin System:** Organize your bot's functionality into independent, reusable plugins. +* **Command Handling:** Easily register commands and extract arguments. +* **Middleware Support:** Run code before or after commands (e.g., logging, access control). +* **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). +* **Context-Aware:** Pass custom database or state contexts to your handlers. +* **Fluent Interface:** Chain methods for clean configuration (e.g., `bot.ErrorTemplate(...).AddPlugins(...)`). + +--- + +## 📦 Installation + +```bash +go get git.nix13.pw/scuroneko/laniakea +``` + +## 🚀 Quick Start (with step-by-step explanation) + +Here is a minimal echo/ping bot example with detailed comments. +```go +package main + +import ( + "log" + + "git.nix13.pw/scuroneko/laniakea" // Import the Laniakea library +) + +// echo is a command handler function. +// It receives two parameters: +// - 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) +func echo(ctx *laniakea.MsgContext, db *laniakea.NoDB) { + // 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.Answer(ctx.Text) // User input WITHOUT command +} + +func main() { + // 1. Create bot options. Replace "TOKEN" with your actual bot token from @BotFather. + opts := &laniakea.BotOpts{Token: "TOKEN"} + + // 2. Initialize a new bot instance. + // We use laniakea.NoDB as the database context type (no database needed for this example). + bot := laniakea.NewBot[laniakea.NoDB](opts) + // Ensure bot resources are cleaned up on exit. + defer bot.Close() + + // 3. Create a new plugin named "ping". + // Plugins help group related commands and middlewares. + p := laniakea.NewPlugin[laniakea.NoDB]("ping") + + // 4. Add a command to the plugin. + // p.NewCommand(echo, "echo") creates a command that triggers the 'echo' function on the "/echo" command. + p.AddCommand(p.NewCommand(echo, "echo")) + + // 5. Add another command using an anonymous function (closure). + // This command simply replies "Pong" when the user sends "/ping". + p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, db *laniakea.NoDB) { + ctx.Answer("Pong") + }, "ping")) + + // 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). + // AddPlugins(p) registers our "ping" plugin with the bot. + bot = bot.ErrorTemplate("Error\n\n%s").AddPlugins(p) + + // 7. Automatically generate commands like /start, /help, and a list of all registered commands. + // This is optional but very useful for most bots. + if err := bot.AutoGenerateCommands(); err != nil { + log.Println(err) + } + + // 8. Start the bot, listening for updates (long polling). + bot.Run() +} +``` + +### How It Works +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. +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)), 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. +6. `ErrorTemplate`: Sets a template for error messages. The %s placeholder is replaced by the actual error. +7. `AutoGenerateCommands`: Adds built-in commands (/start, /help) and a command that lists all available commands. +8. `Run()`: Starts the bot's update polling loop. + +## 📖 Core Concepts +### Plugins + +Plugins are the main way to organize code. A plugin can have multiple commands and middlewares. +```go +plugin := laniakea.NewPlugin[MyDB]("admin") +plugin.AddCommand(plugin.NewCommand(banUser, "ban")) +bot.AddPlugins(plugin) +``` + +### Commands + +A command is a function that handles a specific bot command (e.g., /start). +```go +func myHandler(ctx *laniakea.MsgContext, db *MyDB) { + // Access command arguments via ctx.Args ([]string) + // Reply to the user: ctx.Answer("some text") +} +``` + +### MsgContext + +Provides access to the incoming message and useful reply methods: + +- `Answer(text string)`: Sends a plain text message, automatically escaping MarkdownV2. +- `AnswerMarkdown(text string)`: Sends a message formatted with MarkdownV2 (you handle escaping). +- `AnswerText(text string)`: Sends a message with no parse_mode. +- `SendChatAction(action string)`: Sends a "typing", "uploading photo", etc., action. +- Fields: `Text`, `Args`, `From`, `Chat`, `Msg`, etc. + +### Database Context + +The `T` in `NewBot[T]` is a powerful feature. You can pass any type (like a database connection pool) and it will be available in every command and middleware handler. + +```go +type MyDB struct { /* ... */ } +db := &MyDB{...} +bot := laniakea.NewBot[*MyDB](opts, db) // Pass db instance +``` + +## 🧩 Middleware +Middleware are functions that run before a command handler. They are perfect for cross-cutting concerns like logging, access control, rate limiting, or modifying the context. + +### Signature +A middleware function has the same signature as a command handler, but it must return a bool: + +```go +func(ctx *MsgContext, db T) bool +``` + +- If it returns true, the next middleware (or the command) will be executed. +- If it returns false, the execution chain stops immediately (the command will not run). + +### Adding Middleware +Use the Use method of a plugin to add one or more middleware functions. They are executed in the order they are added. + +```go +plugin := laniakea.NewPlugin[MyDB]("admin") +plugin.Use(loggingMiddleware, adminOnlyMiddleware) +plugin.AddCommand(plugin.NewCommand(banUser, "ban")) +``` + +### Example Middlewares + +1. Logging Middleware – logs every command execution. +```go +func loggingMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { + log.Printf("User %d executed command: %s", ctx.FromID, ctx.Msg.Text) + return true // continue to next middleware/command +} +``` + +2. Admin-Only Middleware – restricts access to users with a specific role. +```go +func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { + if !db.IsAdmin(ctx.FromID) { // assume db has IsAdmin method + ctx.Answer("⛔ Access denied. Admins only.") + return false // stop execution + } + return true +} +``` + +### Important Notes +- Middleware can modify the MsgContext (e.g., add custom fields) before the command runs. +- If you need to run code after a command, you can call it from within the command itself or use a defer statement inside the middleware that wraps the next call (more advanced). + +## ⚙️ Advanced Configuration +- **Inline Keyboards**: Build keyboards using laniakea.NewKeyboard() and AddRow(). +- **Rate Limiting**: Pass a configured utils.RateLimiter via BotOpts to handle Telegram's rate limits gracefully. +- **Custom HTTP Client**: Provide your own http.Client in BotOpts for fine-tuned control. + +## 📝 License + +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. + +## 📚 Learn More +[GoDoc](https://pkg.go.dev/git.nix13.pw/scuroneko/laniakea) + +[Telegram Bot API](https://core.telegram.org/bots/api) + + ✅ Built with ❤️ by scuroneko diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 0000000..000fe90 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,202 @@ +# Laniakea + +[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&style=flat-square)](https://go.dev/) +[![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg?style=flat-square)](LICENSE) +![Gitea Release](https://img.shields.io/gitea/v/release/ScuroNeko/Laniakea?gitea_url=https%3A%2F%2Fgit.nix13.pw&sort=semver&display_name=release&style=flat-square&color=purple&link=https%3A%2F%2Fgit.nix13.pw%2FScuroNeko%2FLaniakea%2Freleases) + +Легковесная, простая в использовании и производительная обёртка для Telegram Bot API на Go. Она упрощает разработку ботов благодаря чистой системе плагинов, поддержке中间件, автоматической генерации команд и встроенному ограничителю скорости запросов. + +[English](README.md) + +--- + +## ✨ Возможности + +* **Простой и интуитивный API:** Разработан для лёгкости использования, основан на практических примерах. +* **Система плагинов:** Организуйте функциональность бота в независимые, переиспользуемые плагины. +* **Обработка команд:** Легко регистрируйте команды и извлекайте аргументы. +* **Поддержка промежуточных слоёв (Middleware):** Выполняйте код до или после команд (например, логирование, проверка доступа). +* **Автоматическая генерация команд:** Генерируйте справку и списки команд автоматически. +* **Встроенный ограничитель запросов (Rate Limiter):** Защитите бота от превышения лимитов Telegram API (с обработкой `retry_after`). +* **Контекст данных:** Передавайте свой контекст базы данных или состояния в обработчики. +* **Текучий интерфейс (Fluent Interface):** Стройте цепочки методов для чистой конфигурации (например, `bot.ErrorTemplate(...).AddPlugins(...)`). + +--- + +## 📦 Установка + +```bash +go get git.nix13.pw/scuroneko/laniakea +``` + +## 🚀 Быстрый старт (с пошаговыми комментариями) +Вот минимальный пример бота "echo/ping" с подробными комментариями. + +```go +package main + +import ( + "log" + + "git.nix13.pw/scuroneko/laniakea" // Импортируем библиотеку Laniakea +) + +// echo — это функция-обработчик команды. +// Она получает два параметра: +// - ctx: контекст сообщения (содержит информацию о сообщении, отправителе, чате и т.д.) +// - db: ваш пользовательский контекст базы данных (здесь мы используем NoDB — заглушку) +func echo(ctx *laniakea.MsgContext, db *laniakea.NoDB) { + // Отвечаем пользователю текстом, который он прислал, без префикса команды. + // ctx.Text содержит сообщение пользователя, из которого удалена часть с командой. + ctx.Answer(ctx.Text) // Ввод пользователя БЕЗ команды +} + +func main() { + // 1. Создаём опции бота. Замените "TOKEN" на реальный токен от @BotFather. + opts := &laniakea.BotOpts{Token: "TOKEN"} + + // 2. Инициализируем новый экземпляр бота. + // Используем laniakea.NoDB как тип контекста базы данных (база не нужна для примера). + bot := laniakea.NewBot[laniakea.NoDB](opts) + // Гарантируем освобождение ресурсов бота при выходе. + defer bot.Close() + + // 3. Создаём новый плагин с именем "ping". + // Плагины помогают группировать связанные команды и промежуточные обработчики. + p := laniakea.NewPlugin[laniakea.NoDB]("ping") + + // 4. Добавляем команду в плагин. + // p.NewCommand(echo, "echo") создаёт команду, которая вызывает функцию 'echo' по команде "/echo". + p.AddCommand(p.NewCommand(echo, "echo")) + + // 5. Добавляем ещё одну команду, используя анонимную функцию (замыкание). + // Эта команда просто отвечает "Pong", когда пользователь отправляет "/ping". + p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, db *laniakea.NoDB) { + ctx.Answer("Pong") + }, "ping")) + + // 6. Настраиваем бота: задаём шаблон ошибки и добавляем плагин. + // ErrorTemplate устанавливает формат для сообщений об ошибках (где %s будет заменён на текст ошибки). + // AddPlugins(p) регистрирует наш плагин "ping" в боте. + bot = bot.ErrorTemplate("Ошибка\n\n%s").AddPlugins(p) + + // 7. Автоматически генерируем команды, такие как /start, /help и список всех зарегистрированных команд. + // Это необязательно, но очень полезно для большинства ботов. + if err := bot.AutoGenerateCommands(); err != nil { + log.Println(err) + } + + // 8. Запускаем бота, начиная прослушивание обновлений (long polling). + bot.Run() +} +``` + +### Как это работает +1. `BotOpts`: Содержит конфигурацию, например, токен API. +2. `NewBot[T]`: Создаёт экземпляр бота. Параметр типа T позволяет передать пользовательский контекст базы данных (например, *sql.DB), который будет доступен во всех обработчиках. Используйте laniakea.NoDB, если он не нужен. +3. `NewPlugin`: Создаёт логическую группу для команд и Middleware. +4. `AddCommand`: Регистрирует команду. Первый аргумент — функция-обработчик (func(*MsgContext, T)), второй — имя команды (без слеша). +5. **Функции-обработчики**: Получают *MsgContext (детали сообщения, методы типа Answer) и ваш контекст базы данных T. +6. `ErrorTemplate`: Устанавливает шаблон для сообщений об ошибках. Плейсхолдер %s заменяется на текст ошибки. +7. `AutoGenerateCommands`: Добавляет встроенные команды (/start, /help) и команду, показывающую список всех доступных команд. +8. `Run()`: Запускает цикл опроса обновлений бота. + +## 📖 Основные концепции +### Плагины (Plugins) +Плагины — основной способ организации кода. Плагин может содержать несколько команд и Middleware. + +```go +plugin := laniakea.NewPlugin[MyDB]("admin") +plugin.AddCommand(plugin.NewCommand(banUser, "ban")) +bot.AddPlugins(plugin) +``` + +### Команды (Commands) +Команда — это функция, которая обрабатывает конкретную команду бота (например, /start). + +```go +func myHandler(ctx *laniakea.MsgContext, db *MyDB) { + // Доступ к аргументам команды через ctx.Args ([]string) + // Ответ пользователю: ctx.Answer("какой-то текст") +} +``` + +### Контекст сообщения (MsgContext) +Предоставляет доступ к входящему сообщению и полезные методы для ответа: + +- `Answer(text string)`: Отправляет обычный текст, автоматически экранируя MarkdownV2. +- `AnswerMarkdown(text string)`: Отправляет сообщение, отформатированное MarkdownV2 (экранирование на вашей стороне). +- `AnswerText(text string)`: Отправляет сообщение без parse_mode. +- `SendChatAction(action string)`: Отправляет действие "печатает", "загружает фото" и т.д. +- Поля: `Text`, `Args`, `From`, `Chat`, `Msg` и другие. + +### Контекст базы данных (Database Context) +Параметр типа `T` в `NewBot[T]` — мощная функция. Вы можете передать любой тип (например, пул соединений с БД), и он будет доступен в каждом обработчике команды и中间件. + +```go +type MyDB struct { /* ... */ } +db := &MyDB{...} +bot := laniakea.NewBot[*MyDB](opts, db) // Передаём экземпляр db +``` + +## 🧩 Промежуточные слои (Middleware) +Middleware — это функции, которые выполняются перед обработчиком команды. Они идеально подходят для сквозных задач, таких как логирование, контроль доступа, ограничение скорости запросов или модификация контекста. + +### Сигнатура +Функция middleware имеет ту же сигнатуру, что и обработчик команды, но должна возвращать bool: + +```go +func(ctx *MsgContext, db T) bool +``` + +- Если возвращается true, выполняется следующий middleware (или сама команда). +- Если возвращается false, цепочка выполнения немедленно прерывается (команда не запускается). + +### Добавление middleware +Используйте метод Use плагина для добавления одной или нескольких функций middleware. Они выполняются в порядке добавления. + +```go +plugin := laniakea.NewPlugin[MyDB]("admin") +plugin.Use(loggingMiddleware, adminOnlyMiddleware) +plugin.AddCommand(plugin.NewCommand(banUser, "ban")) +``` + +### Примеры middleware + +1. Логирующий middleware – логирует каждое выполнение команды. +```go +func loggingMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { + log.Printf("Пользователь %d выполнил команду: %s", ctx.FromID, ctx.Msg.Text) + return true // продолжаем к следующему middleware/команде +} +``` + +2. Middleware только для администраторов – ограничивает доступ пользователям с определённой ролью. +```go +func adminOnlyMiddleware(ctx *laniakea.MsgContext, db *MyDB) bool { + if !db.IsAdmin(ctx.FromID) { // предполагается, что db имеет метод IsAdmin + ctx.Answer("⛔ Доступ запрещён. Только для администраторов.") + return false // останавливаем выполнение + } + return true +} +``` + +### Важные замечания +- Middleware может изменять MsgContext (например, добавлять пользовательские поля) перед запуском команды. +- Если нужно выполнить код после команды, это можно сделать внутри самой команды или использовать отложенный вызов (defer) в middleware, который оборачивает следующий вызов (более продвинутый подход). + +## ⚙️ Расширенная настройка +**Инлайн-клавиатуры**: Создавайте клавиатуры с помощью laniakea.NewKeyboard() и AddRow(). +**Ограничение запросов**: Передайте настроенный utils.RateLimiter через BotOpts для корректной обработки лимитов Telegram. +**Пользовательский HTTP-клиент**: Предоставьте свой http.Client в BotOpts для точного контроля. + +## 📝 Лицензия +Этот проект лицензирован под GNU General Public License v3.0 - подробности см. в файле [LICENSE](LICENSE). + +## 📚 Дополнительная информация +[GoDoc Laniakea](https://pkg.go.dev/git.nix13.pw/scuroneko/laniakea) + +[Telegram Bot API](https://core.telegram.org/bots/api) + + ✅ Создано с ❤️ scuroneko diff --git a/bot.go b/bot.go index f775387..673db98 100644 --- a/bot.go +++ b/bot.go @@ -226,19 +226,24 @@ func NewBot[T any](opts *BotOpts) *Bot[T] { updateQueue := make(chan *tgapi.Update, 512) - var limiter *utils.RateLimiter - if opts.RateLimit > 0 { - limiter = utils.NewRateLimiter() - } + //var limiter *utils.RateLimiter + //if opts.RateLimit > 0 { + // limiter = utils.NewRateLimiter() + //} + limiter := utils.NewRateLimiter() apiOpts := tgapi.NewAPIOpts(opts.Token). SetAPIUrl(opts.APIUrl). UseTestServer(opts.UseTestServer). SetLimiter(limiter) api := tgapi.NewAPI(apiOpts) - uploader := tgapi.NewUploader(api) + prefixes := opts.Prefixes + if len(prefixes) == 0 { + prefixes = []string{"/"} + } + bot := &Bot[T]{ updateOffset: 0, errorTemplate: "%s", @@ -246,7 +251,7 @@ func NewBot[T any](opts *BotOpts) *Bot[T] { api: api, uploader: uploader, debug: opts.Debug, - prefixes: opts.Prefixes, + prefixes: prefixes, token: opts.Token, plugins: make([]Plugin[T], 0), updateTypes: make([]tgapi.UpdateType, 0), @@ -277,7 +282,7 @@ func NewBot[T any](opts *BotOpts) *Bot[T] { if bot.username == "" { bot.logger.Warn("Can't get bot username. Named command handlers won't work!") } - bot.logger.Infof("Authorized as %s (@%s)\n", u.FirstName, Val(u.Username, "unknown")) + bot.logger.Infoln(fmt.Sprintf("Authorized as %s (@%s)", u.FirstName, Val(u.Username, "unknown"))) return bot } diff --git a/examples/basic/example.go b/examples/basic/example.go index e556d7a..f8c5b41 100644 --- a/examples/basic/example.go +++ b/examples/basic/example.go @@ -6,20 +6,22 @@ import ( "git.nix13.pw/scuroneko/laniakea" ) -func pong(ctx *laniakea.MsgContext, db *laniakea.NoDB) { - ctx.Answer(ctx.Msg.Text) +func echo(ctx *laniakea.MsgContext, db *laniakea.NoDB) { + ctx.Answer(ctx.Text) // User input WITHOUT command } func main() { - bot := laniakea.NewBot[laniakea.NoDB](laniakea.LoadOptsFromEnv()) + opts := &laniakea.BotOpts{Token: "TOKEN"} + bot := laniakea.NewBot[laniakea.NoDB](opts) defer bot.Close() p := laniakea.NewPlugin[laniakea.NoDB]("ping") - p.NewCommand(pong, "ping") + p.AddCommand(p.NewCommand(echo, "echo")) + p.AddCommand(p.NewCommand(func(ctx *laniakea.MsgContext, db *laniakea.NoDB) { + ctx.Answer("Pong") + }, "ping")) - bot = bot.ErrorTemplate( - "Error\n\n%s", - ).AddPlugins(p) + bot = bot.ErrorTemplate("Error\n\n%s").AddPlugins(p) if err := bot.AutoGenerateCommands(); err != nil { log.Println(err) diff --git a/examples/basic/go.mod b/examples/basic/go.mod index de6a9b2..19feb1a 100644 --- a/examples/basic/go.mod +++ b/examples/basic/go.mod @@ -2,7 +2,11 @@ module example/basic go 1.26.1 -require git.nix13.pw/scuroneko/laniakea v1.0.0-beta.12 +require git.nix13.pw/scuroneko/laniakea v1.0.0-beta.13 + +replace ( + git.nix13.pw/scuroneko/laniakea v1.0.0-beta.13 => ../../ +) require ( git.nix13.pw/scuroneko/extypes v1.2.1 // indirect diff --git a/examples/basic/go.sum b/examples/basic/go.sum index b3af86c..0b840ff 100644 --- a/examples/basic/go.sum +++ b/examples/basic/go.sum @@ -1,7 +1,7 @@ git.nix13.pw/scuroneko/extypes v1.2.1 h1:IYrOjnWKL2EAuJYtYNa+luB1vBe6paE8VY/YD+5/RpQ= git.nix13.pw/scuroneko/extypes v1.2.1/go.mod h1:uZVs8Yo3RrYAG9dMad6qR6lsYY67t+459D9c65QAYAw= -git.nix13.pw/scuroneko/laniakea v1.0.0-beta.12 h1:IpcLF5OTZKOsYhj7AULDsDPrCUdtSnS5LgApOyMIRYU= -git.nix13.pw/scuroneko/laniakea v1.0.0-beta.12/go.mod h1:M8jwm195hzAl9bj9Bkl95WfHmWvuBX6micsdtOs/gmE= +git.nix13.pw/scuroneko/laniakea v1.0.0-beta.13 h1:mRVxYh7CNrm8ccob+u6XxLzZRbs1fLNRg/nXaXY78yw= +git.nix13.pw/scuroneko/laniakea v1.0.0-beta.13/go.mod h1:M8jwm195hzAl9bj9Bkl95WfHmWvuBX6micsdtOs/gmE= git.nix13.pw/scuroneko/slog v1.0.2 h1:vZyUROygxC2d5FJHUQM/30xFEHY1JT/aweDZXA4rm2g= git.nix13.pw/scuroneko/slog v1.0.2/go.mod h1:3Qm2wzkR5KjwOponMfG7TcGSDjmYaFqRAmLvSPTuWJI= github.com/alitto/pond/v2 v2.7.0 h1:c76L+yN916m/DRXjGCeUBHHu92uWnh/g1bwVk4zyyXg= diff --git a/utils/version.go b/utils/version.go index 80ac2e7..ddcda17 100644 --- a/utils/version.go +++ b/utils/version.go @@ -1,9 +1,9 @@ package utils const ( - VersionString = "1.0.0-beta.13" + VersionString = "1.0.0-beta.14" VersionMajor = 1 VersionMinor = 0 VersionPatch = 0 - VersionBeta = 13 + VersionBeta = 14 )