REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
4
Commands and Plugins
ScuroNeko edited this page 2026-05-20 13:19:27 +03:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Commands and Plugins

Russian version: Commands-and-Plugins-RU

Laniakea organizes most bot behavior through plugins.

If you understand how plugins, commands, payload handlers, and update handlers fit together, the rest of the library becomes much easier to reason about.

The important model first

The normal layering is:

  • Bot owns runtime, update flow, logging, and API clients
  • Plugin groups related handlers
  • commands handle text commands like /start
  • payload handlers handle inline-button callback payloads
  • update handlers handle non-command Telegram updates

In practice, most bots start with:

  • one or more plugins
  • a few commands
  • optional plugin middleware
  • maybe payload handlers once inline keyboards appear

Plugins

A plugin is a named group of:

  • commands
  • payload handlers
  • update handlers
  • shared middleware
  • optional plugin logger and close hook

Example:

plugin := laniakea.NewPlugin[laniakea.NoData]("admin")

Use plugins to group functionality by concern:

  • admin
  • payments
  • profile
  • support

That keeps command registration and middleware ownership clear.

Command handlers

The command handler signature is:

func(ctx *laniakea.MessageContext, db T) error

Where:

  • ctx is the current message/update context
  • db is the dependency value of the bots generic type T

Return:

  • nil on success
  • error when the centralized bot error flow should handle failure

Example:

func start(ctx *laniakea.MessageContext, db *App) error {
	ctx.Answer("Welcome")
	return nil
}

Registering commands

Create a command with NewCommand(...) and add it to a plugin:

plugin := laniakea.NewPlugin[*App]("main")
plugin.AddCommand(plugin.NewCommand(start, "start"))

The command name:

  • must not include the slash
  • is matched against the parsed command token

So:

  • "start" matches /start
  • "help" matches /help

The easiest command example

func echo(ctx *laniakea.MessageContext, db laniakea.NoData) error {
	if ctx.Text == "" {
		ctx.Answer("Usage: /echo <text>")
		return nil
	}

	ctx.Answer(ctx.Text)
	return nil
}

plugin.AddCommand(plugin.NewCommand(echo, "echo"))

For /echo hello world:

  • ctx.Text == "hello world"
  • ctx.Args == []string{"hello", "world"}

Commands with argument validation

You can declare command arguments using CommandArg.

Example:

plugin.AddCommand(
	plugin.NewCommand(banUser, "ban",
		laniakea.NewCommandArg("user_id").
			SetValueType(laniakea.CommandValueIntType).
			SetRequired(),
	),
)

This lets the framework validate:

  • required argument presence
  • integer/string/bool shape
  • custom regex-based restrictions through the argument configuration

If validation fails, the command does not run and the bot error path is used.

Payload handlers

Payload handlers are for callback data coming from inline keyboard buttons.

Register them with NewPayload(...) or AddPayload(...):

func confirmDelete(ctx *laniakea.MessageContext, db *App) error {
	ctx.EditCallback("Deleted", nil)
	return nil
}

plugin.AddPayload(plugin.NewPayload(confirmDelete, "delete.confirm"))

Payload handlers:

  • are triggered by callback payload command names
  • use the same handler signature as normal commands
  • receive parsed payload args in ctx.Args

Use payloads when the trigger source is a button press, not a text command.

Update handlers

Update handlers are for Telegram updates outside the normal command/payload flow.

Register them with AddUpdateHandler(...):

plugin.AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *laniakea.MessageContext, db *App) error {
	// handle inline query here
	return nil
})

This is the right tool for update types like:

  • inline_query
  • chosen_inline_result
  • poll
  • chat_member
  • other non-command updates

Important:

  • message
  • channel_post
  • callback_query

stay on the command/payload flow and are not meant to be registered through AddUpdateHandler(...).

The runtime flow

For text commands:

  1. Telegram update arrives
  2. bot prepares MessageContext
  3. bot middleware runs
  4. matching plugin is found
  5. plugin middleware runs
  6. command argument validation runs
  7. command-specific middleware runs
  8. command handler runs
  9. returned error, if any, goes through centralized error handling

For callback payloads, the same idea applies, except the trigger comes from decoded callback data instead of text command parsing.

Middleware placement

You have two main middleware levels:

Plugin middleware

Added with:

plugin.AddMiddleware(...)

Use this for logic shared by most handlers in the plugin.

Command-specific middleware

Added with:

plugin.NewCommand(handler, "name").Use(middleware)

Use this when only one command or payload needs the check.

See Middleware for behavior details.

Good plugin boundaries

Good plugin grouping usually follows one of these patterns:

  • by business domain: billing, admin, profile
  • by update source: inline, support, moderation
  • by ownership: one plugin per subsystem or package

Avoid one giant plugin for the entire bot unless the bot is very small.

Examples

Example: admin command with plugin middleware

admin := laniakea.NewPlugin[*App]("admin")
admin.AddMiddleware(laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MessageContext, app *App) bool {
	if !app.IsAdmin(ctx.FromID) {
		ctx.Answer("Access denied")
		return false
	}
	return true
}))

admin.AddCommand(admin.NewCommand(func(ctx *laniakea.MessageContext, app *App) error {
	ctx.Answer("Banned")
	return nil
}, "ban"))

Example: payload handler for inline keyboard callback

plugin.AddPayload(plugin.NewPayload(func(ctx *laniakea.MessageContext, app *App) error {
	ctx.AnswerCbQueryText("Accepted")
	ctx.EditCallback("Done", nil)
	return nil
}, "approve"))

Common mistakes

Putting the slash into command names

Wrong:

plugin.NewCommand(start, "/start")

Right:

plugin.NewCommand(start, "start")

Treating payloads like commands

Payloads are not matched from text messages. They come from button callback data.

Using AddUpdateHandler(...) for command updates

Reserved update types like message, channel_post, and callback_query belong to the normal command/payload pipeline.

Returning errors for normal user-facing branching

Not every branch is an error. For normal usage failures, often the better pattern is:

ctx.Answer("Usage: /ban <id>")
return nil

Return an error when the failure is genuinely exceptional or when you want the centralized error flow.

When to use what

Use:

  • commands for slash-prefixed user messages
  • payload handlers for inline button callbacks
  • update handlers for other Telegram update types
  • plugin middleware for shared checks
  • command middleware for narrow, local checks

Where to go next