botbooter

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 13 Imported by: 0

README

botbooter

Go Reference CI Go Report Card Go Version

A small, framework-style toolkit for writing chat bots once and running them on Slack, Discord, Telegram, WhatsApp, or a local CLI — with the same handlers, middleware, and attachment access on every platform.

Inspired by Gin: you register pattern-matched command handlers and optional middleware, then run the bot. botbooter abstracts the platform behind a single Bot type so your business logic does not care whether a message came from Slack, Discord, Telegram, WhatsApp, or stdin.

⚠️ Not production ready. botbooter is pre-1.0 and under active development. The public API may change without notice, and it has not been hardened or battle-tested for production workloads. Use it for experiments and side projects; pin a specific version and review changes before depending on it for anything critical.

Features

  • One API, multiple platforms — Slack (Socket Mode), Discord (Gateway), Telegram (long polling), WhatsApp (Cloud API webhook), and a built-in CLI adapter for local development and testing with no credentials.
  • Regex command routing — patterns are compiled once and matched against message content; first match wins.
  • Middleware chain — wrap every message (logging, auth, metrics, …) with next-style composition.
  • Platform-agnostic attachments — read image/file attachments uniformly across platforms.
  • Context-first & graceful shutdown — handlers receive a context.Context; Run(ctx) / Start() connect and shut down cleanly on cancellation or SIGINT/SIGTERM.
  • Resilient dispatch — a panicking handler is recovered and logged instead of taking down the bot.

Install

go get github.com/lao/botbooter

Requires Go 1.23+.

Quickstart

The fastest way to try it — the CLI adapter needs no tokens:

package main

import (
	"context"
	"os"
	"strings"

	"github.com/lao/botbooter"
)

func main() {
	bot := botbooter.InitAsCLIBot(os.Stdin, os.Stdout)

	_ = bot.HandleFunc("^echo ", func(ctx context.Context, b *botbooter.Bot, m *botbooter.Message) {
		_ = b.SendMessageContext(ctx, m.ChannelID, strings.TrimPrefix(m.Content, "echo "))
	})

	ctx, stop := context.WithCancel(context.Background())
	defer stop()
	_ = bot.Run(ctx) // type "echo hi", press enter; Ctrl-D to quit
}

Or run the bundled example directly:

go run ./examples/v1            # CLI mode (default, no credentials)
go run ./examples/v1 slack      # uses SLACK_APP_TOKEN / SLACK_BOT_TOKEN
go run ./examples/v1 discord    # uses DISCORD_BOT_TOKEN
go run ./examples/v1 telegram   # uses TELEGRAM_BOT_TOKEN
go run ./examples/v1 whatsapp   # uses WA_TOKEN / WA_PHONE_ID / WA_APP_SECRET / WA_VERIFY_TOKEN / WA_ADDR (+ optional WA_PATH)

Concepts

Constructing a bot
Constructor Signature Notes
InitAsCLIBot(in io.Reader, out io.Writer) *Bot Local adapter; nil defaults to stdin/stdout.
InitAsSlackBot(appToken, botToken string) *Bot Socket Mode (xapp-… + xoxb-…).
InitAsDiscordBot(token string) (*Bot, error) Enables the message-content intent (see below).
InitAsTelegramBot(token string) (*Bot, error) Long polling via getUpdates; BotFather token.
InitAsWhatsAppBot(cfg WhatsAppConfig) (*Bot, error) Meta Cloud API; runs an inbound webhook HTTP server.
Handlers, commands and middleware
// A command routes messages whose content matches a regular expression.
_ = bot.AddHandler(botbooter.Command{
	Pattern: "^ping$",
	Handler: func(ctx context.Context, b *botbooter.Bot, m *botbooter.Message) {
		_ = b.SendMessageContext(ctx, m.ChannelID, "pong")
	},
})

// HandleFunc is a shorthand for the common case.
_ = bot.HandleFunc("^hello", greetHandler)

// Fallback when nothing matches.
bot.SetUnknownCommandHandler(func(ctx context.Context, b *botbooter.Bot, m *botbooter.Message) {
	_ = b.SendMessageContext(ctx, m.ChannelID, "unknown command")
})

// Middleware wraps dispatch; call next to continue the chain.
bot.AddMiddleware(func(ctx context.Context, b *botbooter.Bot, m *botbooter.Message, next botbooter.CommandHandler) {
	log.Printf("%s in %s: %s", m.UserID, m.ChannelID, m.Content)
	next(ctx, b, m)
})

AddHandler / HandleFunc return an error if the pattern is not a valid regular expression.

Attachments
attachments, err := b.GetAttachments(m)
for _, a := range attachments {
	fmt.Println(a.URL, a.IsImage) // a.ExtraData holds the raw platform payload
}

Attachment.URL is empty on platforms that deliver media by id (Telegram, WhatsApp). Call b.ResolveAttachmentURL(ctx, a) for a downloadable link on any platform — Discord/CLI return a.URL as-is, while Slack/Telegram/WhatsApp resolve one on demand. The Telegram link embeds the bot token (a one-line warning logs on each resolve, suppressible via BOTBOOTER_TELEGRAM_SUPPRESS_URL_WARNING); see docs/platforms.md.

A terminal has no real upload channel, so the CLI adapter treats any local file path in the message as an attachment — "uploading" means referencing the path. Image files are detected by content sniffing:

echo here is my screenshot /tmp/cat.png
  → attachment (image): /tmp/cat.png
Message fields

Every Message carries normalized, platform-agnostic fields so handlers rarely need the raw event. UserID, ChannelID and Content are always set; the rest are best-effort and stay at their zero value when a platform cannot supply them:

Field Meaning
ID Platform message id ("" for CLI).
AuthorName Display/username (empty on Slack, which delivers only an id).
Timestamp Message time as a time.Time (zero on CLI).
ReplyToID Id of the replied-to/thread message ("" when not a reply).
MentionedUserIDs Mentioned user ids; Telegram contributes only text_mention ids.
Raw platform access

When you need something the normalized fields don't carry, reach the originating event or the underlying SDK client through typed accessors — internal/core stays free of every platform SDK, so these live on the facade:

if ev, ok := botbooter.SlackRawEvent(m); ok {
	_ = ev.ThreadTimeStamp // anything on the raw *slackevents.MessageEvent
}

// Raw event per platform: DiscordRawEvent, SlackRawEvent, TelegramRawEvent, WhatsAppRawEvent, CLIRawEvent.
// Underlying client per platform (WhatsApp has none — it speaks the Cloud API over plain HTTP):
client := botbooter.SlackClient(bot)         // *slack.Client (nil if not a Slack bot)
session := botbooter.DiscordSession(bot)     // *discordgo.Session
tg := botbooter.TelegramClient(bot)          // *bot.Bot
Lifecycle
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

if err := bot.Run(ctx); err != nil { // connect, serve, and shut down on cancel
	log.Fatal(err)
}
  • Run(ctx) — connect, block until ctx is canceled (or the event loop ends), then disconnect cleanly.
  • Start() — shorthand for Run with a context bound to SIGINT/SIGTERM.
  • Connect(ctx) / Disconnect() — non-blocking control if you want to manage the loop yourself. Disconnect is idempotent.

Platform setup

Each platform takes different credentials. Full step-by-step setup, troubleshooting, and the official documentation for each live in docs/platforms.md.

Platform What you need Setup
Slack xapp-… app-level token + xoxb-… bot token docs/platforms.md
Discord bot token + Message Content Intent docs/platforms.md
Telegram BotFather bot token docs/platforms.md
WhatsApp Cloud API token + phone-number id + app secret + verify token + bind addr docs/platforms.md
CLI nothing (local stdin/stdout) docs/platforms.md

Development

make all        # fmt + vet + lint + test
make test-race  # race detector
make cover      # coverage report
make run-cli    # run the example bot in CLI mode

The suite runs under the race detector and is hermetic by default. The single test that touches the Slack network is opt-in, enabled by setting the BOTBOOTER_SLACK_NETWORK_TEST environment variable (see botbooter_test.go).

DEMO

for slack and discord:

https://user-images.githubusercontent.com/197033/229368894-19b366d3-ca6d-41d2-9ab7-ca8e1a53b31a.mov

Why

Alternatives:

Joe-bot
  • no support for Discord
  • no generic access for attachments in messages
GoSarah
  • no support for Discord
  • no generic access for attachments in messages

Roadmap

  • Slack, Discord, Telegram, WhatsApp and CLI adapters
  • Middleware and attachment abstraction
  • Microsoft Teams adapter
  • Richer message types (blocks, embeds)
  • Unify attachment url retriavel for all implementations

Contributing

Issues and PRs are welcome. Please run make all (format, vet, lint, race tests) before opening a PR.

License

MIT © Lucas Abreu Oliveira

Documentation

Overview

Package botbooter is a small framework for building chat bots that behave the same way across Slack, Discord, Telegram, WhatsApp and a local CLI. It is a thin facade over the internal packages, so consumers keep a single import path.

Example
package main

import (
	"context"
	"os"
	"strings"

	"github.com/lao/botbooter"
)

func main() {
	bot := botbooter.InitAsCLIBot(strings.NewReader("echo hello\n"), os.Stdout)

	_ = bot.HandleFunc("^echo ", func(ctx context.Context, b *botbooter.Bot, m *botbooter.Message) {
		_ = b.SendMessageContext(ctx, m.ChannelID, strings.TrimPrefix(m.Content, "echo "))
	})

	// Run returns when the input reaches EOF.
	_ = bot.Run(context.Background())
}
Output:
hello

Index

Examples

Constants

View Source
const (
	SlackBotType    = core.SlackBotType
	DiscordBotType  = core.DiscordBotType
	CLIBotType      = core.CLIBotType
	TelegramBotType = core.TelegramBotType
	WhatsAppBotType = core.WhatsAppBotType
)

Supported bot types.

View Source
const TelegramEnvSuppressURLWarning = telegram.EnvSuppressURLWarning

TelegramEnvSuppressURLWarning names the environment variable that silences the plaintext-token warning logged on every successful Telegram resolve via [Bot.ResolveAttachmentURL]. Set it to any non-empty value to opt out.

Variables

View Source
var (
	ErrUnknownBotType   = core.ErrUnknownBotType
	ErrAlreadyConnected = core.ErrAlreadyConnected
	// ErrMissingWhatsAppConfig is returned by InitAsWhatsAppBot when a required
	// WhatsAppConfig field is empty.
	ErrMissingWhatsAppConfig = whatsapp.ErrMissingConfig
)

Errors returned by Bot methods and platform helpers.

Functions

func DiscordRawEvent added in v0.2.0

func DiscordRawEvent(m *Message) (*discordgo.MessageCreate, bool)

DiscordRawEvent returns the raw Discord event carried on m, reporting whether m originated from Discord.

func DiscordSession added in v0.2.0

func DiscordSession(b *Bot) *discordgo.Session

DiscordSession returns the discordgo session backing b, or nil if b is not a Discord bot.

func SlackClient added in v0.2.0

func SlackClient(b *Bot) *slackapi.Client

SlackClient returns the Slack Web API client backing b, or nil if b is not a Slack bot.

func SlackRawEvent added in v0.2.0

func SlackRawEvent(m *Message) (*slackevents.MessageEvent, bool)

SlackRawEvent returns the raw Slack event carried on m, reporting whether m originated from Slack.

func SlackSocketClient added in v0.2.0

func SlackSocketClient(b *Bot) *socketmode.Client

SlackSocketClient returns the Socket Mode client backing b, or nil if b is not a Slack bot.

func TelegramClient added in v0.2.0

func TelegramClient(b *Bot) *bot.Bot

TelegramClient returns the go-telegram bot client backing b, or nil if b is not a Telegram bot.

func TelegramRawEvent added in v0.2.0

func TelegramRawEvent(m *Message) (*models.Update, bool)

TelegramRawEvent returns the raw Telegram update carried on m, reporting whether m originated from Telegram.

Types

type Attachment

type Attachment = core.Attachment

Attachment is a platform-agnostic file attachment. See core.Attachment.

type Bot

type Bot = core.Bot

Bot is the platform-agnostic chat bot. See core.Bot.

func InitAsCLIBot

func InitAsCLIBot(in io.Reader, out io.Writer) *Bot

InitAsCLIBot creates a local CLI bot.

func InitAsDiscordBot

func InitAsDiscordBot(token string) (*Bot, error)

InitAsDiscordBot creates a Discord bot that connects via the Gateway.

func InitAsSlackBot

func InitAsSlackBot(appToken, botToken string) *Bot

InitAsSlackBot creates a Slack bot that connects via Socket Mode.

func InitAsTelegramBot added in v0.2.0

func InitAsTelegramBot(token string) (*Bot, error)

InitAsTelegramBot creates a Telegram bot that connects via the Bot API.

func InitAsWhatsAppBot added in v0.2.0

func InitAsWhatsAppBot(cfg WhatsAppConfig) (*Bot, error)

InitAsWhatsAppBot creates a WhatsApp bot backed by the Meta Cloud API. It runs an inbound webhook HTTP server at cfg.Addr, so put a TLS-terminating proxy in front and register the public HTTPS URL in Meta's webhook settings. Inbound media arrives as an id in Attachment.ExtraData (not a URL); resolve the bytes with GET /{media-id} using your access token. It returns an error if a required config field is missing.

type BotType

type BotType = core.BotType

BotType identifies the messaging platform a Bot is connected to.

type CLIMessage

type CLIMessage = core.CLIMessage

CLIMessage is the raw payload of a CLI message. See core.CLIMessage.

func CLIRawEvent added in v0.2.0

func CLIRawEvent(m *Message) (*CLIMessage, bool)

CLIRawEvent returns the parsed CLI line carried on m, reporting whether m originated from the CLI adapter.

type Command

type Command = core.Command

Command pairs a regexp pattern with a handler. See core.Command.

type CommandHandler

type CommandHandler = core.CommandHandler

CommandHandler handles a matched message. See core.CommandHandler.

type Message

type Message = core.Message

Message is an incoming message handed to handlers. See core.Message.

type Middleware

type Middleware = core.Middleware

Middleware wraps message dispatch. See core.Middleware.

type WhatsAppConfig added in v0.2.0

type WhatsAppConfig = whatsapp.Config

WhatsAppConfig configures a WhatsApp Cloud API bot. See whatsapp.Config.

type WhatsAppMedia added in v0.2.0

type WhatsAppMedia = whatsapp.Media

WhatsAppMedia identifies media attached to a WhatsApp message. See whatsapp.Media.

type WhatsAppMessage added in v0.2.0

type WhatsAppMessage = whatsapp.Message

WhatsAppMessage is the parsed payload of a WhatsApp message. See whatsapp.Message.

func WhatsAppRawEvent added in v0.2.0

func WhatsAppRawEvent(m *Message) (*WhatsAppMessage, bool)

WhatsAppRawEvent returns the parsed WhatsApp message carried on m, reporting whether m originated from WhatsApp. AuthorName and Timestamp on the returned value are enriched, not present in its Raw JSON.

Directories

Path Synopsis
examples
v1 command
Command v1 is a small demo of botbooter.
Command v1 is a small demo of botbooter.
internal
asserts
Package asserts holds tiny test assertion helpers shared across botbooter.
Package asserts holds tiny test assertion helpers shared across botbooter.
cli
Package cli is the local CLI adapter for botbooter.
Package cli is the local CLI adapter for botbooter.
core
Package core holds botbooter's platform-agnostic engine: the Bot type, its command/middleware dispatch, and the connection lifecycle.
Package core holds botbooter's platform-agnostic engine: the Bot type, its command/middleware dispatch, and the connection lifecycle.
discord
Package discord is the Discord adapter for botbooter.
Package discord is the Discord adapter for botbooter.
slack
Package slack is the Slack adapter for botbooter, connecting via Socket Mode.
Package slack is the Slack adapter for botbooter, connecting via Socket Mode.
telegram
Package telegram is the Telegram adapter for botbooter: it connects via the Bot API getUpdates long-poll loop and implements core.Adapter.
Package telegram is the Telegram adapter for botbooter: it connects via the Bot API getUpdates long-poll loop and implements core.Adapter.
whatsapp
Package whatsapp is the WhatsApp adapter for botbooter.
Package whatsapp is the WhatsApp adapter for botbooter.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL