botapi_fsm

package module
v0.0.0-...-b298f20 Latest Latest
Warning

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

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 9 Imported by: 0

README

botapi-fsm

Minimal finite-state machine (FSM) layer for github.com/gotd/botapi.

  • Generic machine: Machine[S, D]
  • Pluggable storage: memory, Redis, SQL, etc.
  • Native botapi integration: Mount, MountGroup, Active, InState

Install

go get github.com/fluffur/botapi-fsm

Quick Start

type state string

const (
	idle    state = ""
	askName state = "name"
)

type data struct{ Name string }

m := fsm.NewMemory[state, data](idle)

m.Register(askName, func(c *botapi.Context, sess *fsm.Session[state, data]) error {
	sess.Data.Name = c.Message().Text
	return m.Clear(c)
})

pm := bot.Group(botapi.ChatTypeIs(botapi.ChatTypePrivate))

pm.OnCommand("profile", "Start profile flow", func(c *botapi.Context) error {
	if err := m.Enter(c, askName, data{}); err != nil {
		return err
	}
	_, err := c.Reply("What is your name? /cancel to abort.")
	return err
})

m.MountGroup(pm) // message + callback + /cancel

Redis Store

client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
store := fsm.NewRedisJSONStore[state, data](client, "bot:fsm:", 24*time.Hour)
m := fsm.New[state, data](store, idle)

Documentation

Overview

Package botapi_fsm provides a finite-state machine layer for github.com/gotd/botapi.

It is storage-agnostic: bring your own Store (memory, Redis, Postgres, …) or use NewMemory for a process-local machine. State and payload types are generic so each bot defines its own states and data shape.

Quick start:

type state string

const (
	idle           state = ""
	awaitingName   state = "name"
)

m := fsm.NewMemory[state, struct{}](idle)
m.Register(awaitingName, func(c *botapi.Context, sess *fsm.Session[state, struct{}]) error {
	// sess.Data, c.Message().Text, …
	return m.Clear(c)
})

pm := bot.Group(botapi.ChatTypeIs(botapi.ChatTypePrivate))
pm.OnCommand("profile", "Start profile wizard", func(c *botapi.Context) error {
	if err := m.Enter(c, awaitingName, struct{}{}); err != nil {
		return err
	}
	_, err := c.Reply("What is your name? /cancel to abort.")
	return err
})
m.MountGroup(pm)

Mount registers message and callback handlers guarded by Machine.Active so only users with a non-idle session are routed into the FSM. Register FSM handlers before generic fallbacks — botapi dispatches the first match.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoSessionKey = errors.New("fsm: no session key in context")

ErrNoSessionKey is returned when a context carries no session key.

Functions

func ChatSenderKey

func ChatSenderKey(c *botapi.Context) (int64, bool)

ChatSenderKey scopes sessions to a chat and user pair (for group FSMs).

func ChatSenderUpdateKey

func ChatSenderUpdateKey(u *botapi.Update) (int64, bool)

ChatSenderUpdateKey is the update-level counterpart of ChatSenderKey.

func MustPingRedis

func MustPingRedis(ctx context.Context, client redis.Cmdable) error

MustPingRedis verifies connectivity and returns detailed error.

func PrivateChatKey

func PrivateChatKey(c *botapi.Context) (int64, bool)

PrivateChatKey scopes sessions to the private-chat user id. Prefer SenderKey; use this explicitly when you only ever run in PM.

func SenderKey

func SenderKey(c *botapi.Context) (int64, bool)

SenderKey uses the update sender's user id (messages, callbacks, inline). In private chats it falls back to chat id when From is absent — that can happen with some MTProto updates even though chat id equals the user id.

func SenderUpdateKey

func SenderUpdateKey(u *botapi.Update) (int64, bool)

SenderUpdateKey is the update-level counterpart of SenderKey.

func WithSession

func WithSession[S comparable, D any](ctx context.Context, sess Session[S, D]) context.Context

WithSession attaches a session to ctx for downstream handlers and middleware.

Types

type BytesStore

type BytesStore interface {
	Get(ctx context.Context, key int64) ([]byte, bool, error)
	Set(ctx context.Context, key int64, value []byte) error
	Clear(ctx context.Context, key int64) error
}

BytesStore is a minimal key-value backend for encoded sessions.

type CancelHandler

type CancelHandler[S comparable, D any] func(c *botapi.Context, sess *Session[S, D]) error

CancelHandler runs after a session is cleared via the cancel command.

type Handler

type Handler[S comparable, D any] func(c *botapi.Context, sess *Session[S, D]) error

Handler processes an update while the user is in a specific state.

type KeyFunc

type KeyFunc func(c *botapi.Context) (int64, bool)

KeyFunc extracts a session key from a handler context. The default is SenderKey.

type Machine

type Machine[S comparable, D any] struct {
	// contains filtered or unexported fields
}

Machine routes botapi updates to per-state handlers.

func New

func New[S comparable, D any](store Store[S, D], idle S, opts ...Option[S, D]) *Machine[S, D]

New builds a machine backed by store. idle is the zero/active-outside-FSM state.

func NewMemory

func NewMemory[S comparable, D any](idle S, opts ...Option[S, D]) *Machine[S, D]

NewMemory is shorthand for an in-memory machine.

func (*Machine[S, D]) Active

func (m *Machine[S, D]) Active() botapi.Predicate

Active is a predicate that matches updates for users currently inside the FSM.

func (*Machine[S, D]) Clear

func (m *Machine[S, D]) Clear(c *botapi.Context) error

Clear removes the session for the context's key.

func (*Machine[S, D]) Enter

func (m *Machine[S, D]) Enter(c *botapi.Context, state S, data D) error

Enter starts or replaces a session.

func (*Machine[S, D]) Get

func (m *Machine[S, D]) Get(c *botapi.Context) (Session[S, D], bool, error)

Get returns the current session for the context's key.

func (*Machine[S, D]) Handler

func (m *Machine[S, D]) Handler() botapi.Handler

Handler returns a botapi handler that dispatches by current session state. Pair it with Machine.Active (as Machine.Mount does) so idle users are not consumed.

func (*Machine[S, D]) InState

func (m *Machine[S, D]) InState(states ...S) botapi.Predicate

InState matches updates whose session is one of the given states.

func (*Machine[S, D]) Middleware

func (m *Machine[S, D]) Middleware() botapi.Middleware

Middleware loads the current session into the context without handling the update.

func (*Machine[S, D]) Mount

func (m *Machine[S, D]) Mount(bot *botapi.Bot, predicates ...botapi.Predicate)

Mount registers FSM handlers on bot (messages + callback queries). Extra predicates narrow the scope (e.g. private chats only).

func (*Machine[S, D]) MountGroup

func (m *Machine[S, D]) MountGroup(g *botapi.Group, predicates ...botapi.Predicate)

MountGroup registers FSM handlers on a handler group.

func (*Machine[S, D]) Register

func (m *Machine[S, D]) Register(state S, h Handler[S, D])

Register binds a state to its handler.

func (*Machine[S, D]) Set

func (m *Machine[S, D]) Set(c *botapi.Context, sess Session[S, D]) error

Set stores a session for the context's key.

func (*Machine[S, D]) Warm

func (m *Machine[S, D]) Warm(ctx context.Context, keys []int64) error

Warm loads all sessions from store into the in-process cache. Call after restart when using a persistent store so predicates work immediately.

type MemoryStore

type MemoryStore[S comparable, D any] struct {
	// contains filtered or unexported fields
}

MemoryStore keeps sessions in process memory.

func NewMemoryStore

func NewMemoryStore[S comparable, D any]() *MemoryStore[S, D]

NewMemoryStore returns an empty in-memory store.

func (*MemoryStore[S, D]) Clear

func (s *MemoryStore[S, D]) Clear(_ context.Context, key int64) error

func (*MemoryStore[S, D]) Get

func (s *MemoryStore[S, D]) Get(_ context.Context, key int64) (Session[S, D], bool, error)

func (*MemoryStore[S, D]) Set

func (s *MemoryStore[S, D]) Set(_ context.Context, key int64, sess Session[S, D]) error

type Option

type Option[S comparable, D any] func(*Machine[S, D])

Option configures a Machine.

func WithCancelCommand

func WithCancelCommand[S comparable, D any](cmd string) Option[S, D]

WithCancelCommand registers a command that clears the active session. Pass "" to disable. Default is "cancel".

func WithKeyFunc

func WithKeyFunc[S comparable, D any](f KeyFunc) Option[S, D]

WithKeyFunc sets how session keys are derived from handler contexts.

func WithOnCancel

func WithOnCancel[S comparable, D any](h CancelHandler[S, D]) Option[S, D]

WithOnCancel is called after /cancel clears a non-idle session.

func WithOnUnknown

func WithOnUnknown[S comparable, D any](h UnknownHandler[S, D]) Option[S, D]

WithOnUnknown is called when a session references an unregistered state.

func WithUpdateKeyFunc

func WithUpdateKeyFunc[S comparable, D any](f UpdateKeyFunc) Option[S, D]

WithUpdateKeyFunc sets how session keys are derived from updates (for predicates). Must agree with WithKeyFunc on the same updates.

type RedisBytesStore

type RedisBytesStore struct {
	// contains filtered or unexported fields
}

RedisBytesStore persists FSM payloads in Redis. Keys are stored as "<prefix><session-key>" and values are raw bytes.

func NewRedisBytesStore

func NewRedisBytesStore(client redis.Cmdable, prefix string, ttl time.Duration) *RedisBytesStore

NewRedisBytesStore builds a Redis-backed BytesStore. If ttl is zero, keys do not expire.

func (*RedisBytesStore) Clear

func (s *RedisBytesStore) Clear(ctx context.Context, key int64) error

func (*RedisBytesStore) Get

func (s *RedisBytesStore) Get(ctx context.Context, key int64) ([]byte, bool, error)

func (*RedisBytesStore) Set

func (s *RedisBytesStore) Set(ctx context.Context, key int64, value []byte) error

type Session

type Session[S comparable, D any] struct {
	State S
	Data  D
}

Session is a user's place in the state machine.

func SessionFromContext

func SessionFromContext[S comparable, D any](ctx context.Context) (Session[S, D], bool)

SessionFromContext returns a session previously stored with WithSession.

type Store

type Store[S comparable, D any] interface {
	Get(ctx context.Context, key int64) (Session[S, D], bool, error)
	Set(ctx context.Context, key int64, sess Session[S, D]) error
	Clear(ctx context.Context, key int64) error
}

Store persists FSM sessions. Implement it for Redis, SQL, or any backend.

func JSONStore

func JSONStore[S comparable, D any](inner BytesStore) Store[S, D]

JSONStore adapts BytesStore to Store using JSON encoding.

func NewRedisJSONStore

func NewRedisJSONStore[S comparable, D any](
	client redis.Cmdable,
	prefix string,
	ttl time.Duration,
) Store[S, D]

NewRedisJSONStore is a convenience constructor for JSON-encoded sessions.

type UnknownHandler

type UnknownHandler[S comparable, D any] func(c *botapi.Context, sess Session[S, D]) error

UnknownHandler runs when stored state has no registered handler (session is cleared first).

type UpdateKeyFunc

type UpdateKeyFunc func(u *botapi.Update) (int64, bool)

UpdateKeyFunc extracts a session key from a raw update. Used by predicates; must stay consistent with KeyFunc.

Directories

Path Synopsis
examples
chat command
memory-store command
redis-store command

Jump to

Keyboard shortcuts

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