# How to Extend Zakirullin Files Bot Functionality with Custom Plugins

> Extend Zakirullin Files bot functionality by creating custom plugins. Learn how to implement the BotPlugin interface, register your plugin, and follow testing patterns.

- Repository: [Artem Zakirullin/files.md](https://github.com/zakirullin/files.md)
- Tags: how-to-guide
- Published: 2026-05-21

---

**You extend the Files bot by implementing the `BotPlugin` interface with `CanHandle()` and `Handle()` methods, registering your plugin in the global `BotPlugins` slice located in [`server/bot.go`](https://github.com/zakirullin/files.md/blob/main/server/bot.go), and following the unit-testing patterns established by the reference `WorldClockPlugin` implementation.**

The Zakirullin files.md Telegram bot provides a lightweight, compile-time plugin system that intercepts incoming messages before standard command processing begins. To extend Zakirullin Files bot functionality with custom plugins, you create a Go package in `server/plugins/`, satisfy the two-method interface contract, and append your constructor to the `BotPlugins` registry. This architecture allows you to add arbitrary functionality—ranging from simple text transformers to complex third-party API integrations—while maintaining a clean separation of concerns.

## Understanding the Core Plugin Architecture

The plugin system revolves around three primary components defined in [`server/bot.go`](https://github.com/zakirullin/files.md/blob/main/server/bot.go).

**`BotPlugin` Interface** — Located at lines 15–18, this interface declares the contract every plugin must satisfy:

```go
type BotPlugin interface {
    CanHandle(string) bool        // Lightweight filter to claim the message
    Handle(string) (string, error) // Heavy lifting to generate the reply
}

```

**`BotPlugins` Slice** — Defined at lines 43–44, this global variable holds all active plugin instances. The bot evaluates plugins in the order they appear in this slice.

**Dispatch Loop** — Inside `Bot.Reply` (lines 42–58), the bot iterates over `BotPlugins` and routes the message to the first plugin whose `CanHandle` method returns `true`. Once a plugin handles the message, the bot skips standard command and path processing, sending the plugin’s output directly to the user.

## Step-by-Step Guide to Building a Custom Plugin

### Step 1: Create the Plugin File

Create a new Go file inside `server/plugins/`, for example [`server/plugins/calculator.go`](https://github.com/zakirullin/files.md/blob/main/server/plugins/calculator.go). This file must declare a struct that implements the `BotPlugin` interface and expose a constructor function following the `New…Plugin()` naming convention.

### Step 2: Implement the Interface Methods

Your implementation must provide two methods with distinct responsibilities:

- **`CanHandle(msg string) bool`** — Perform fast, stateless checks such as regex matching or prefix detection. This method runs for every incoming message, so keep it lightweight.
- **`Handle(msg string) (string, error)`** — Execute the core logic, including external API calls, calculations, or database queries. Return the formatted response string or an error if processing fails.

### Step 3: Register the Plugin

Open [`server/bot.go`](https://github.com/zakirullin/files.md/blob/main/server/bot.go) and locate the `BotPlugins` slice declaration. Append your plugin’s constructor to the slice:

```go
var (
    BotPlugins = []BotPlugin{
        plugins.NewWorldClockPlugin(),
        plugins.NewCalculatorPlugin(), // Your custom plugin
    }
)

```

Registration occurs at compile time, so you must rebuild the binary after adding your plugin.

### Step 4: Write Unit Tests

Create a corresponding test file, such as [`server/plugins/calculator_test.go`](https://github.com/zakirullin/files.md/blob/main/server/plugins/calculator_test.go), using the `testify/require` library to assert behavior:

```go
package plugins

import (
    "testing"
    "github.com/stretchr/testify/require"
)

func TestCalculatorPlugin_Handle(t *testing.T) {
    r := require.New(t)
    p := NewCalculatorPlugin()

    out, err := p.Handle("/calc 2+2")
    r.NoError(err)
    r.Contains(out, "2+2")
}

```

Run `go test ./...` from the repository root to verify your plugin does not break existing functionality.

## Complete Plugin Skeleton

Below is a minimal, runnable template demonstrating the required structure. This example reacts to messages starting with `/calc `:

```go
package plugins

import (
    "errors"
    "fmt"
    "regexp"
)

// CalculatorPlugin demonstrates the BotPlugin contract.
type CalculatorPlugin struct{}

// NewCalculatorPlugin exposes the constructor required by server/bot.go.
func NewCalculatorPlugin() *CalculatorPlugin {
    return &CalculatorPlugin{}
}

// CanHandle runs a quick regex check to claim messages.
func (p *CalculatorPlugin) CanHandle(msg string) bool {
    return regexp.MustCompile(`^/calc\s`).MatchString(msg)
}

// Handle performs the heavy work and returns the reply text.
func (p *CalculatorPlugin) Handle(msg string) (string, error) {
    matches := regexp.MustCompile(`^/calc\s+(.+)`).FindStringSubmatch(msg)
    if len(matches) < 2 {
        return "", errors.New("invalid calculation command")
    }
    
    // In production, use a proper math parser; here we echo for demonstration.
    return fmt.Sprintf("Calculation request received: %s", matches[1]), nil
}

```

## Execution Flow and Priority

When a user sends a message, the bot executes the following logic in [`server/bot.go`](https://github.com/zakirullin/files.md/blob/main/server/bot.go) (lines 42–58):

1. **Inline query handling** (if applicable).
2. **Channel handling** (if the message originates from a channel).
3. **Plugin iteration** — The bot loops through `BotPlugins` in declaration order.
4. **First-match wins** — If `CanHandle` returns `true`, the bot calls `Handle`, sends the returned string to the user, cleans any active keyboards, and exits the message handler.

Because the first matching plugin aborts further processing, order matters: place high-priority plugins (like emergency commands) earlier in the `BotPlugins` slice.

## Reference Implementation: WorldClockPlugin

The repository includes a production-grade example at [`server/plugins/world_clock.go`](https://github.com/zakirullin/files.md/blob/main/server/plugins/world_clock.go). This plugin parses natural language date and time strings, resolves locations, and returns formatted world-clock messages. Study this file to understand:

- How to load external data (timezone databases) during plugin initialization.
- Robust error handling inside `Handle`.
- Comprehensive test coverage in [`server/plugins/world_clock_test.go`](https://github.com/zakirullin/files.md/blob/main/server/plugins/world_clock_test.go), which demonstrates table-driven tests for edge cases.

## Summary

- **Implement the interface** — Create `CanHandle()` for fast filtering and `Handle()` for processing logic in a new file under `server/plugins/`.
- **Register in [`server/bot.go`](https://github.com/zakirullin/files.md/blob/main/server/bot.go)** — Append your `New…Plugin()` constructor to the `BotPlugins` slice (lines 43–44).
- **Test thoroughly** — Mirror the `WorldClockPlugin` test suite using `testify/require` to ensure reliability.
- **Respect execution order** — The first plugin claiming a message prevents others from processing it, so structure your `CanHandle` logic precisely.

## Frequently Asked Questions

### What happens if two plugins both return true for CanHandle?

The bot processes `BotPlugins` sequentially and stops at the first match. Only the first plugin’s `Handle` method executes, and its response is sent to the user. Subsequent plugins in the slice never see the message.

### Can I load external configuration or API clients inside a plugin?

Yes. Initialize external clients or configuration structs inside your `New…Plugin()` constructor and store them as fields on your plugin struct. The `Handle` method can then access these resources to make HTTP requests or query databases.

### Do plugins support asynchronous or long-running operations?

The current architecture executes `Handle` synchronously within the message processing loop. Block `Handle` only for short durations, or offload long-running work to a background goroutine that later pushes results via the Telegram Bot API separately.

### Where should I place my plugin’s unit tests?

Place test files in the same `server/plugins/` directory using the [`_test.go`](https://github.com/zakirullin/files.md/blob/main/_test.go) suffix (e.g., [`my_plugin_test.go`](https://github.com/zakirullin/files.md/blob/main/my_plugin_test.go)). Import `github.com/stretchr/testify/require` to align with the existing test suite and run `go test ./server/plugins/...` to validate your changes.