teleflow

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 10 Imported by: 0

README

teleflow

Go Version Go Reference Tests codecov License

Overview

Teleflow is a conversation-flow package for Telebot. It defines ordered, versioned steps, persists session state through a small storage interface, and routes text, callbacks, media, and other Telebot events to active flows. Session state includes typed data and bounded step history.

Installation

go get github.com/hellocashmere/teleflow

Getting Started

The following bot collects a destination and traveler count in two steps:

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/hellocashmere/teleflow"
	"github.com/hellocashmere/teleflow/storage"
	tele "gopkg.in/telebot.v4"
)

func main() {
	ctx, stop := signal.NotifyContext(
		context.Background(),
		os.Interrupt,
		syscall.SIGTERM,
	)
	defer stop()

	pref := tele.Settings{
		Token:  os.Getenv("TOKEN"),
		Poller: &tele.LongPoller{Timeout: 10 * time.Second},
	}

	bot, err := tele.NewBot(pref)
	if err != nil {
		log.Fatal(err)
	}

	bus := teleflow.NewBus(storage.NewMemory())

	bot.Handle(tele.OnText, bus.Handle)
	bot.Handle("/trip", TripHandler(ctx, bus))

	go func() {
		<-ctx.Done()
		bot.Stop()
	}()

	log.Println("telegram bot started successfully")
	bot.Start()
}

func TripHandler(ctx context.Context, bus teleflow.Bus) tele.HandlerFunc {
	trip := bus.NewFlow(teleflow.FlowConfig{
		Name:        "trip",
		Version:     1,
		IdleTimeout: 15 * time.Minute,
	})

	trip.Step(
		"destination",
		tele.OnText,
		func(c tele.Context) error {
			return c.Send("Where do you want to go?")
		},
		func(c tele.Context, fc teleflow.Context) (teleflow.StepResult, error) {
			destination := strings.TrimSpace(c.Text())
			if destination == "" {
				return fc.Stay(), c.Send("Please enter a destination.")
			}

			if err := fc.SetString("destination", destination); err != nil {
				return fc.Stay(), err
			}

			return fc.Next(), nil
		},
	)

	trip.Step(
		"travelers",
		tele.OnText,
		func(c tele.Context) error {
			return c.Send("How many people are traveling?")
		},
		func(c tele.Context, fc teleflow.Context) (teleflow.StepResult, error) {
			travelers, err := strconv.Atoi(strings.TrimSpace(c.Text()))
			if err != nil || travelers < 1 {
				return fc.Stay(), c.Send("Please enter a positive whole number.")
			}

			if err := fc.SetInt("travelers", travelers); err != nil {
				return fc.Stay(), err
			}

			destination, _ := fc.GetString("destination")
			if err := c.Send(fmt.Sprintf(
				"Trip to %s saved for %d travelers.",
				destination,
				travelers,
			)); err != nil {
				return fc.Stay(), err
			}

			return fc.Next(), nil
		},
	)

	return func(c tele.Context) error {
		if err := trip.Build(); err != nil {
			return fmt.Errorf("build trip flow: %w", err)
		}

		return bus.Start(ctx, c, trip)
	}
}

bus.Handle receives updates for active flows using a background context. The command handler builds the flow definition before calling Start. Start creates a session when none exists, resumes a pending step-entry callback, or leaves an active session unchanged. Returning Next from the final step completes the flow and removes its stored state.

Register bus.Handle for every Telebot event used by the steps. For example, an inline-button flow also needs bot.Handle(tele.OnCallback, bus.Handle).

Context

Step callbacks work with two different contexts:

  • tele.Context contains the current Telegram update and methods such as Send, Edit, Text, Message, and Callback.
  • teleflow.Context contains the current flow state, typed data, metadata, and transition methods.

Use teleflow.Context to pass data between steps:

if err := fc.SetString("company", "Acme"); err != nil {
	return fc.Stay(), err
}

company, ok := fc.GetString("company")

Typed methods are available for strings, booleans, integers, floating-point numbers, durations, and times. Delete removes a value, while Has checks whether a key exists.

The same context controls the next transition:

return fc.Stay(), nil
return fc.Next(), nil
return fc.Back(), nil
return fc.Go("company_details"), nil

Current identifies the step whose handler is running, while Step reads the active step from the working session state. Depth reports the retained history depth at the start of the callback, and CanBack reports whether that history contained a previous step. A teleflow.Context belongs to one callback. Its data changes are persisted only when the callback returns a valid transition without an error and the updated state is written successfully.

The standard library context.Context controls cancellation and deadlines for storage operations and context-aware callbacks. It is accepted by Bus.Start, Bus.HandleCtx, and Bus.Cancel, and passed to storage methods. Only callbacks registered with Flow.StepContext receive it; callbacks registered with Flow.Step do not. Use Bus.HandleCtx(ctx) instead of Bus.Handle when update handling must observe cancellation or deadlines:

handler := bus.HandleCtx(ctx)

bot.Handle(tele.OnText, handler)
bot.Handle(tele.OnCallback, handler)

Use Flow.StepContext together with Start(ctx, ...) and HandleCtx(ctx) when step callbacks must observe cancellation:

flow.StepContext(
	"company_details",
	tele.OnText,
	func(ctx context.Context, c tele.Context) error {
		return c.Send("Tell us about your company.")
	},
	func(
		ctx context.Context,
		c tele.Context,
		fc teleflow.Context,
	) (teleflow.StepResult, error) {
		if err := ctx.Err(); err != nil {
			return fc.Stay(), err
		}

		return fc.Next(), nil
	},
)

License

Teleflow is distributed under the MIT License.

Documentation

Index

Constants

View Source
const (
	// DefaultIdleTimeout is the maximum session inactivity when FlowConfig.IdleTimeout is zero.
	DefaultIdleTimeout = 24 * time.Hour

	// DefaultHistoryLimit is the retained step count when FlowConfig.HistoryLimit is zero.
	DefaultHistoryLimit = 64

	// MaxRuntimeStateSize is the largest serialized session state that teleflow reads or writes.
	MaxRuntimeStateSize = 1 << 20
)

Variables

View Source
var (
	ErrNameEmpty               = errors.New("flow: name cannot be empty")
	ErrVersionInvalid          = errors.New("flow: version must be greater than zero")
	ErrIdleTimeoutNegative     = errors.New("flow: idle timeout cannot be negative")
	ErrHistoryLimitNegative    = errors.New("flow: history limit cannot be negative")
	ErrNilDefinition           = errors.New("flow: nil definition")
	ErrNoStepsDefined          = errors.New("flow: no steps defined")
	ErrDefinitionExists        = errors.New("flow: definition already exists")
	ErrExpired                 = errors.New("flow: expired")
	ErrDefinitionNotRegistered = errors.New("flow: definition is not registered")
	ErrInvalidRuntimeState     = errors.New("flow: invalid runtime state")
	ErrContextNoSender         = errors.New("flow: context has no sender")
	ErrInvalidStepResult       = errors.New("flow: invalid step result")
	ErrStorageNil              = errors.New("flow: storage is nil")
	ErrTargetStepNotFound      = errors.New("flow: target step not found")
)
View Source
var (
	ErrDataKeyEmpty = errors.New("flow: data key cannot be empty")
	ErrInvalidValue = errors.New("flow: invalid value")
)
View Source
var (
	ErrFlowNotBuilt     = errors.New("flow: definition is not built")
	ErrStepNameEmpty    = errors.New("flow: step name cannot be empty")
	ErrStepEventEmpty   = errors.New("flow: step event cannot be empty")
	ErrStepDuplicate    = errors.New("flow: duplicate step")
	ErrStepHandlerNil   = errors.New("flow: step handler cannot be nil")
	ErrRuntimeStateSize = errors.New("flow: runtime state is too large")
)

Functions

This section is empty.

Types

type BeginContextFunc

type BeginContextFunc func(context.Context, tele.Context) error

BeginContextFunc is the context-aware form of BeginFunc.

type BeginFunc

type BeginFunc func(tele.Context) error

BeginFunc runs when a session enters a step.

Teleflow may call it again when an earlier attempt or the following state write did not complete.

type Bus

type Bus interface {
	// NewFlow creates a mutable flow draft.
	//
	// Add steps and call Build before passing the flow to Start.
	//
	// Example:
	//
	//	flow := bus.NewFlow(FlowConfig{
	//		Name:    "signup",
	//		Version: 1,
	//	})
	NewFlow(config FlowConfig) *Flow

	// Start creates a session for a built flow.
	//
	// The flow must have been created by this Bus and successfully built.
	// If a session already exists, Start resumes the stored session's pending begin callback and otherwise leaves it unchanged.
	// An expired session is removed and reported as ErrExpired.
	//
	// Example:
	//
	//	if err := flow.Build(); err != nil {
	//		log.Fatal(err)
	//	}
	//
	//	bot.Handle("/signup", func(c tele.Context) error {
	//		return bus.Start(ctx, c, flow)
	//	})
	Start(ctx context.Context, c tele.Context, flow *Flow) error

	// Handle processes an update for an active session using context.Background.
	//
	// Register it for every Telebot event used by the flow.
	// Updates without an active session or a matching current-step event are ignored.
	//
	// Example:
	//
	//	bot.Handle(tele.OnText, bus.Handle)
	Handle(c tele.Context) error

	// HandleCtx returns a Telebot handler that uses ctx for every update.
	//
	// Register the returned handler for every Telebot event used by the flow.
	//
	// Example:
	//
	//	handler := bus.HandleCtx(ctx)
	//	bot.Handle(tele.OnText, handler)
	//	bot.Handle(tele.OnCallback, handler)
	HandleCtx(ctx context.Context) tele.HandlerFunc

	// Cancel removes the active session identified by the Telebot context.
	//
	// It succeeds when no session exists.
	//
	// Example:
	//
	//	bot.Handle("/cancel", func(c tele.Context) error {
	//		return bus.Cancel(ctx, c)
	//	})
	Cancel(ctx context.Context, c tele.Context) error
}

Bus registers flow definitions and processes their sessions.

It coordinates concurrent updates only within one Bus instance.

func NewBus

func NewBus(store storage.Storage) Bus

NewBus creates a flow bus backed by store.

A nil store is accepted, but session operations then return ErrStorageNil.

The bus serializes updates for each session within this Bus instance. Other Bus instances are not coordinated, even when they share a store.

type Context

type Context interface {
	// SetString stores a string under the key.
	SetString(string, string) error

	// SetBool stores a boolean under the key.
	SetBool(string, bool) error

	// SetInt stores an int value normalized to int64.
	SetInt(string, int) error

	// SetInt32 stores an int32 value normalized to int64.
	SetInt32(string, int32) error

	// SetInt64 stores an int64 under the key.
	SetInt64(string, int64) error

	// SetFloat32 stores a finite float32 value normalized to float64.
	SetFloat32(string, float32) error

	// SetFloat64 stores a finite float64 under the key.
	SetFloat64(string, float64) error

	// SetDuration stores a time.Duration as an int64 nanosecond count.
	SetDuration(string, time.Duration) error

	// SetTime stores a time.Time in RFC3339Nano format.
	SetTime(string, time.Time) error

	// Delete removes the value stored under the key.
	// It returns ErrDataKeyEmpty for an empty key and succeeds when the key is absent.
	Delete(string) error

	// Has reports whether the key exists in the current flow data.
	Has(string) bool

	// GetString returns the stored string and reports whether the key exists with that type.
	GetString(string) (string, bool)

	// GetBool returns the stored boolean and reports whether the key exists with that type.
	GetBool(string) (bool, bool)

	// GetInt returns the stored integer and reports whether it exists and fits in an int.
	GetInt(string) (int, bool)

	// GetInt32 returns the stored integer and reports whether it exists and fits in an int32.
	GetInt32(string) (int32, bool)

	// GetInt64 returns the stored int64 and reports whether the key exists with that type.
	GetInt64(string) (int64, bool)

	// GetFloat32 returns the stored number and reports whether it exists and converts without overflow or underflow.
	GetFloat32(string) (float32, bool)

	// GetFloat64 returns the stored float64 and reports whether the key exists with that type.
	GetFloat64(string) (float64, bool)

	// GetDuration returns the stored duration and reports whether the key exists with that type.
	GetDuration(string) (time.Duration, bool)

	// GetTime returns the stored time and reports whether the key contains a valid RFC3339Nano value.
	GetTime(string) (time.Time, bool)

	// Current returns the step whose handler is running.
	Current() string

	// Depth returns the number of retained history entries at the start of the handler call.
	Depth() int

	// CanBack reports whether a previous history entry existed at the start of the handler call.
	CanBack() bool

	// Step returns the active step recorded in the working session state.
	Step() string

	// Next requests the next defined step.
	// On the last step, successful handling completes the flow and removes the session state.
	Next() StepResult

	// Back requests the previous retained history entry.
	// At the first retained entry, the flow stays on the current step.
	Back() StepResult

	// Go requests the named step.
	// Handling returns ErrTargetStepNotFound when the flow has no step with that name.
	Go(string) StepResult

	// Stay requests no step change.
	// Successful handling persists any data changes.
	Stay() StepResult
}

Context exposes session data and transition operations during one step handler call.

Data changes are persisted only after the handler returns a valid transition without an error and the state write succeeds. Setters and Delete return ErrDataKeyEmpty when given an empty key.

type Flow

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

Flow describes a versioned flow definition.

A new Flow is a mutable draft. A successful Build registers it and makes it immutable.

func (*Flow) Build

func (f *Flow) Build() error

Build validates the draft and registers it with the bus that created it.

A successful build makes the flow immutable, and later calls to Build return nil.

func (*Flow) Name

func (f *Flow) Name() string

Name returns the configured flow name.

func (*Flow) Step

func (f *Flow) Step(
	name string,
	event string,
	begin BeginFunc,
	handle HandleFunc,
)

Step appends a named step that handles event.

The begin callback is optional, but handle must not be nil when the flow is built. Calls made after a successful Build are ignored.

func (*Flow) StepContext

func (f *Flow) StepContext(
	name string,
	event string,
	begin BeginContextFunc,
	handle HandleContextFunc,
)

StepContext appends a named step whose callbacks receive the operation context.

The begin callback is optional, but handle must not be nil when the flow is built. Calls made after a successful Build are ignored.

func (*Flow) Version

func (f *Flow) Version() uint

Version returns the configured flow version.

type FlowConfig

type FlowConfig struct {
	// Name identifies the flow and must not be empty.
	Name string

	// Version identifies the flow definition and must be greater than zero.
	Version uint

	// IdleTimeout controls how long a session may remain inactive.
	// Zero uses DefaultIdleTimeout, while a negative value is invalid.
	IdleTimeout time.Duration

	// HistoryLimit controls how many step names are retained for Back.
	// Zero uses DefaultHistoryLimit, while a negative value is invalid.
	HistoryLimit int
}

FlowConfig configures a flow draft.

Build validates its values.

type HandleContextFunc

type HandleContextFunc func(context.Context, tele.Context, Context) (StepResult, error)

HandleContextFunc is the context-aware form of HandleFunc.

type HandleFunc

type HandleFunc func(tele.Context, Context) (StepResult, error)

HandleFunc processes a matching event and returns the next transition.

Session data and the transition are persisted only after the callback returns a valid transition without an error and the state write succeeds.

type Step

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

Step describes a named flow step and the Telebot event it handles.

func (*Step) Event

func (s *Step) Event() string

Event returns the Telebot event type handled by the step.

func (*Step) Name

func (s *Step) Name() string

Name returns the stable identifier of the step.

type StepResult

type StepResult uint8

StepResult describes the transition requested by a step handler.

Use the transition methods on Context to obtain a value.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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