How to Extend Zakirullin Files Bot Functionality with Custom Plugins

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, 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.

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

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. 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 and locate the BotPlugins slice declaration. Append your plugin’s constructor to the slice:

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, using the testify/require library to assert behavior:

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 :

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 (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. 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, 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 — 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 suffix (e.g., 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →