core

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package core holds botbooter's platform-agnostic engine: the Bot type, its command/middleware dispatch, and the connection lifecycle.

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyConnected = errors.New("botbooter: already connected")

ErrAlreadyConnected is returned by Connect when the Bot is already connected.

View Source
var ErrNilMessage = errors.New("botbooter: nil message")

ErrNilMessage is returned by Bot methods handed a nil *Message argument.

View Source
var ErrUnknownBotType = errors.New("botbooter: unknown bot type")

ErrUnknownBotType is returned by Bot methods when the Bot has no adapter.

Functions

func AdapterAs added in v0.2.0

func AdapterAs[T any](b *Bot) (T, bool)

AdapterAs returns the Bot's adapter as T, reporting whether it is that type. Adapter packages use it to recover their concrete adapter from a *Bot.

Types

type Adapter

type Adapter interface {
	Connect(ctx context.Context, deps AdapterDeps) error
	Disconnect() error
	Send(ctx context.Context, channelID, text string, opts SendOptions) error
	Attachments(m *Message) ([]Attachment, error)
}

Adapter is the platform-specific half of a Bot. The Bot drives it through this interface, so the core has no compile-time dependency on any platform.

type AdapterDeps

type AdapterDeps struct {
	Dispatch            func(ctx context.Context, m *Message)
	DispatchReaction    func(ctx context.Context, r *Reaction)
	HasReactionHandlers bool
	Done                func(err error)
	Disconnect          func() error
	Logger              *slog.Logger // always non-nil
}

AdapterDeps is the set of callbacks an Adapter uses to talk back to the Bot, plus the Bot's logger so adapter diagnostics route through the same sink. DispatchReaction is optional: adapters on platforms without reaction events simply never call it. HasReactionHandlers reports whether any OnReaction handler was registered before Connect, so an adapter whose reaction ingress costs something (the GitHub poller pays API requests per cycle) can skip the work nobody consumes; push-based adapters may ignore it — dispatching to zero handlers is free. Handlers registered after Connect do not flip it: the snapshot follows the register-before-Connect contract.

type Attachment

type Attachment struct {
	IsImage   bool
	URL       string
	ExtraData any
}

Attachment is a platform-agnostic file attached to a message.

type AttachmentResolver added in v0.2.0

type AttachmentResolver interface {
	ResolveAttachmentURL(ctx context.Context, att Attachment) (string, error)
}

AttachmentResolver is an optional capability an Adapter may implement to turn an Attachment into a downloadable URL; adapters whose Attachment.URL is already usable ride the passthrough in Bot.ResolveAttachmentURL.

type Bot

type Bot struct {
	BotType BotType
	// contains filtered or unexported fields
}

Bot is the platform-agnostic chat bot. Register handlers and middleware before Connect; after that, Connect/Run/Disconnect/Send are safe to call concurrently. Registering after Connect races the dispatch goroutine.

func New

func New(botType BotType, adapter Adapter) *Bot

New creates a Bot of the given type backed by adapter.

func (*Bot) AddHandler

func (b *Bot) AddHandler(cmd Command)

AddHandler registers cmd, compiling its Pattern. An invalid pattern is recorded rather than returned — it surfaces from Connect (and Run) and is also logged at record time (so a registration after Connect is not silently dropped), and the command is not registered. Commands are matched in registration order, first match wins.

func (*Bot) AddMiddleware

func (b *Bot) AddMiddleware(middleware Middleware)

AddMiddleware appends middleware to the dispatch chain, run in registration order.

func (*Bot) Connect

func (b *Bot) Connect(ctx context.Context) error

Connect starts the adapter's event loop and returns without blocking. It returns ErrAlreadyConnected if a connection is already active, ErrUnknownBotType if the Bot has no adapter, every registration error recorded by AddHandler (joined, one per invalid pattern), or any error from the adapter's own Connect.

func (*Bot) Disconnect

func (b *Bot) Disconnect() error

Disconnect tears down the active connection: it cancels the run context and runs the adapter's Disconnect exactly once. It is safe to call when not connected, returning ErrUnknownBotType only if the Bot has no adapter.

func (*Bot) GetAttachments

func (b *Bot) GetAttachments(message *Message) ([]Attachment, error)

GetAttachments returns the platform-agnostic attachments of message. It returns ErrNilMessage if message is nil, or ErrUnknownBotType if the Bot has no adapter.

func (*Bot) HandleFunc

func (b *Bot) HandleFunc(pattern string, handler CommandHandler)

HandleFunc is a convenience wrapper around AddHandler.

func (*Bot) OnReaction added in v0.4.0

func (b *Bot) OnReaction(h ReactionHandler)

OnReaction registers h to run whenever a user adds an emoji reaction, on the platforms that surface reaction events (Slack, Discord, Telegram, WhatsApp, GitHub). Handlers are not regex-matched — branch on Reaction.Emoji inside the handler — and, unlike message dispatch, reactions bypass the Middleware chain. Register before Connect: adapters whose reaction ingress costs something decide at Connect whether to run it (AdapterDeps.HasReactionHandlers) — on GitHub, a handler registered only after Connect means no reaction poller starts and OnReaction never fires for that connection.

func (*Bot) Reply added in v0.3.0

func (b *Bot) Reply(ctx context.Context, m *Message, text string) error

Reply is convenience sugar for replying into the thread or reply-chain of the inbound message m — it is exactly SendMessageContext(ctx, m.ChannelID, text, InReplyTo(m)). Each adapter derives its own platform-specific anchor; see SendOptions. It returns ErrNilMessage if m is nil, or ErrUnknownBotType if the Bot has no adapter.

func (*Bot) ReplyToMessage added in v0.4.0

func (b *Bot) ReplyToMessage(ctx context.Context, channelID, replyToID, text string) error

ReplyToMessage posts text as a reply nested on the message identified by replyToID in channelID. If the Bot's adapter implements ThreadedSender the call is delegated; otherwise it falls back to a plain Bot.SendMessageContext, so the reply still reaches the channel, just not threaded. It returns ErrUnknownBotType if the Bot has no adapter.

func (*Bot) ResolveAttachmentURL added in v0.2.0

func (b *Bot) ResolveAttachmentURL(ctx context.Context, att Attachment) (string, error)

ResolveAttachmentURL returns a downloadable URL for att. If the adapter implements AttachmentResolver the call is delegated; otherwise att.URL is returned verbatim. It returns ErrUnknownBotType if the Bot has no adapter. An empty string with a nil error means "not resolvable", not a failure.

The result is consumed differently per platform:

  • Discord: a signed CDN link (~24h); plain GET, consume promptly.
  • Slack: not directly fetchable — download via the Slack Web API client (SlackClient(b).GetFileContext), which injects the bot token.
  • Telegram: a plain GET on a secret, ~1h URL that embeds the bot token — never log or cache it. Each resolve logs a warning, suppressible via BOTBOOTER_TELEGRAM_SUPPRESS_URL_WARNING.
  • WhatsApp: GET with an "Authorization: Bearer <token>" header (the Cloud API token used to send). Short-lived; consume promptly.
  • Teams: a pre-authorized link carrying a short-lived token — consume promptly, never log or cache. Inline images may need an Authorization header this adapter does not yet supply.
  • CLI: a local filesystem path (open with os.Open), not an HTTP URL.

func (*Bot) Run

func (b *Bot) Run(ctx context.Context) error

Run connects the Bot and blocks until ctx is canceled, the event loop ends, or Disconnect is called from elsewhere, then disconnects. A clean shutdown (ctx cancellation or a local Disconnect) returns nil rather than ctx.Err(), so callers can safely do log.Fatal(bot.Run(ctx)).

func (*Bot) SendMessage

func (b *Bot) SendMessage(channelID, text string, opts ...SendOption) error

SendMessage sends text to channelID using a background context. Prefer SendMessageContext from within a handler so the send honors shutdown and cancellation; SendMessage's background context outlives Run's teardown.

func (*Bot) SendMessageContext

func (b *Bot) SendMessageContext(ctx context.Context, channelID, text string, opts ...SendOption) error

SendMessageContext sends text to channelID, honoring ctx for cancellation. Pass InReplyTo or WithThreadID to thread the message onto an inbound one; with no options it is a plain channel message.

func (*Bot) SetLogger added in v0.3.0

func (b *Bot) SetLogger(logger *slog.Logger)

SetLogger routes the Bot's and its adapter's diagnostics (panic recovery, shutdown warnings, webhook rejections) through logger instead of slog.Default. Like handler registration, call it before Connect.

func (*Bot) SetUnknownCommandHandler

func (b *Bot) SetUnknownCommandHandler(handler CommandHandler)

SetUnknownCommandHandler sets the handler invoked when a message matches no registered command; if unset, unmatched messages are ignored.

func (*Bot) Start

func (b *Bot) Start() error

Start runs the Bot until the process receives an interrupt or SIGTERM.

type BotType

type BotType int

BotType identifies the messaging platform a Bot is connected to.

const (
	SlackBotType BotType = iota
	DiscordBotType
	CLIBotType
	TelegramBotType
	WhatsAppBotType
	TeamsBotType
	WhatsMeowBotType
	GitHubBotType
)

The supported bot types.

func (BotType) String

func (t BotType) String() string

String returns the lowercase platform name for t, or "BotType(n)" for an unknown value.

type Command

type Command struct {
	Pattern string
	Handler CommandHandler
	// contains filtered or unexported fields
}

Command pairs a regular-expression Pattern with the Handler to run on a match.

type CommandHandler

type CommandHandler func(ctx context.Context, b *Bot, m *Message)

CommandHandler handles a dispatched message for a matched command.

type Message

type Message struct {
	ID         string
	UserID     string
	AuthorName string
	ChannelID  string
	Content    string
	Timestamp  time.Time
	ReplyToID  string
	// MentionedUserIDs lists each mentioned user once, in first-mention order.
	// Whether the bot's own mention appears follows Content: Teams strips the
	// bot's mention from Content and so excludes its ID here; Slack and Discord
	// keep it in both.
	MentionedUserIDs []string

	Raw any
}

Message is a platform-agnostic incoming message handed to command handlers. UserID, ChannelID and Content are always set; the remaining normalized fields are best-effort per platform. Raw carries the originating platform's untouched event; read it with the matching typed accessor (e.g. discord.RawEvent).

type Middleware

type Middleware func(ctx context.Context, b *Bot, m *Message, next CommandHandler)

Middleware wraps message dispatch; it must call next to continue the chain.

type Reaction added in v0.4.0

type Reaction struct {
	Emoji      string
	UserID     string
	AuthorName string
	ChannelID  string
	MessageID  string

	Raw any
}

Reaction is a platform-agnostic emoji reaction added to a message, handed to handlers registered with Bot.OnReaction. UserID, ChannelID and MessageID are always set; MessageID identifies the reacted message and is the reply target for Bot.ReplyToMessage. Emoji renders as-is when sent back in a message on its origin platform: a unicode character on most platforms, Slack's colon-wrapped shortname (":thumbsup:", covering custom workspace emojis), Discord's "<:name:id>" markup for its custom emojis. It is NOT normalized across platforms — compare per platform. AuthorName is best-effort and inline-only — empty on platforms whose reaction payload carries only a user id. Raw carries the originating platform event untouched; read it with the matching typed accessor (e.g. slack.RawReaction) to recover the platform's original values, such as the unwrapped emoji name (slack.RawReaction(r).Reaction gives "thumbsup", discord.RawReaction(r).Emoji.Name the bare custom-emoji name).

type ReactionHandler added in v0.4.0

type ReactionHandler func(ctx context.Context, b *Bot, r *Reaction)

ReactionHandler handles an emoji reaction dispatched to Bot.OnReaction.

type SendOption added in v0.3.0

type SendOption func(*SendOptions)

SendOption modifies a SendOptions. Construct them with InReplyTo / WithThreadID and pass them to Bot.SendMessageContext / Bot.SendMessage.

func InReplyTo added in v0.3.0

func InReplyTo(m *Message) SendOption

InReplyTo anchors the send on m so the adapter posts into m's thread or reply-chain, deriving the correct per-platform anchor itself.

func WithThreadID added in v0.3.0

func WithThreadID(id string) SendOption

WithThreadID anchors the send on a raw native id the adapter uses verbatim. It takes precedence over InReplyTo. The caller owns platform-correctness.

type SendOptions added in v0.3.0

type SendOptions struct {
	ReplyTo  *Message
	ThreadID string
}

SendOptions is the resolved set of per-send modifiers an Adapter reads off a Send call. Its zero value means "a plain channel message". A threading anchor is platform-specific, so each adapter derives its own from these fields:

  • ReplyTo: reply anchored on this whole message; the adapter picks the correct native anchor (Slack thread_ts from ReplyToID; Discord/Telegram/ WhatsApp the replied-to/quoted message id).
  • ThreadID: a raw native anchor supplied by the caller, used verbatim. It wins over ReplyTo when both are set. On Slack it is a thread_ts; on Discord/Telegram/WhatsApp a reply/quote message id (NOT a Discord thread-channel id).

type ThreadedSender added in v0.4.0

type ThreadedSender interface {
	SendThreaded(ctx context.Context, channelID, replyToID, text string) error
}

ThreadedSender is an OPTIONAL capability an Adapter may implement to post a reply nested on a specific message. Like AttachmentResolver it is deliberately NOT part of the mandatory Adapter interface: adapters that implement it get threaded replies via Bot.ReplyToMessage; those that do not fall back to a plain channel message.

Jump to

Keyboard shortcuts

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