Documentation
¶
Overview ¶
Package core holds botbooter's platform-agnostic engine: the Bot type, its command/middleware dispatch, and the connection lifecycle.
Index ¶
- Variables
- func AdapterAs[T any](b *Bot) (T, bool)
- type Adapter
- type AdapterDeps
- type Attachment
- type AttachmentResolver
- type Bot
- func (b *Bot) AddHandler(cmd Command)
- func (b *Bot) AddMiddleware(middleware Middleware)
- func (b *Bot) Connect(ctx context.Context) error
- func (b *Bot) Disconnect() error
- func (b *Bot) GetAttachments(message *Message) ([]Attachment, error)
- func (b *Bot) HandleFunc(pattern string, handler CommandHandler)
- func (b *Bot) OnReaction(h ReactionHandler)
- func (b *Bot) Reply(ctx context.Context, m *Message, text string) error
- func (b *Bot) ReplyToMessage(ctx context.Context, channelID, replyToID, text string) error
- func (b *Bot) ResolveAttachmentURL(ctx context.Context, att Attachment) (string, error)
- func (b *Bot) Run(ctx context.Context) error
- func (b *Bot) SendMessage(channelID, text string, opts ...SendOption) error
- func (b *Bot) SendMessageContext(ctx context.Context, channelID, text string, opts ...SendOption) error
- func (b *Bot) SetLogger(logger *slog.Logger)
- func (b *Bot) SetUnknownCommandHandler(handler CommandHandler)
- func (b *Bot) Start() error
- type BotType
- type Command
- type CommandHandler
- type Message
- type Middleware
- type Reaction
- type ReactionHandler
- type SendOption
- type SendOptions
- type ThreadedSender
Constants ¶
This section is empty.
Variables ¶
var ErrAlreadyConnected = errors.New("botbooter: already connected")
ErrAlreadyConnected is returned by Connect when the Bot is already connected.
var ErrNilMessage = errors.New("botbooter: nil message")
ErrNilMessage is returned by Bot methods handed a nil *Message argument.
var ErrUnknownBotType = errors.New("botbooter: unknown bot type")
ErrUnknownBotType is returned by Bot methods when the Bot has no adapter.
Functions ¶
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 ¶
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 (*Bot) AddHandler ¶
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 ¶
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 ¶
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
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
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
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 ¶
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
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.
type BotType ¶
type BotType int
BotType identifies the messaging platform a Bot is connected to.
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 ¶
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
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
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.