CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-aiogram

Modern and fully asynchronous framework for Telegram Bot API

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

state-management.mddocs/

State Management

Built-in Finite State Machine (FSM) support for complex conversational flows with multiple storage backends, state groups, and context management. Enables creating multi-step interactions, forms, and complex bot workflows.

Capabilities

Core FSM Classes

Foundation classes for state management and context handling.

class State:
    """Represents a single state in the FSM"""
    
    def __init__(self, state: str, group_name: str | None = None):
        """
        Initialize a state.
        
        Parameters:
        - state: Unique state identifier
        - group_name: Optional group name (auto-detected from StatesGroup)
        """
    
    @property
    def state(self) -> str:
        """Get the state identifier"""
    
    @property
    def group(self) -> str:
        """Get the state group name"""

class StatesGroup:
    """Base class for grouping related states"""
    
    def __class_getitem__(cls, item: str) -> State:
        """Get state by name"""

class FSMContext:
    """Context for managing FSM state and data"""
    
    async def set_state(self, state: State | str | None = None) -> None:
        """
        Set the current state.
        
        Parameters:
        - state: State to set (None clears state)
        """
    
    async def get_state(self) -> str | None:
        """Get the current state"""
    
    async def clear(self) -> None:
        """Clear state and all associated data"""
    
    async def set_data(self, data: dict[str, Any]) -> None:
        """
        Set context data.
        
        Parameters:
        - data: Data dictionary to store
        """
    
    async def get_data(self) -> dict[str, Any]:
        """Get all context data"""
    
    async def update_data(self, **kwargs: Any) -> None:
        """
        Update context data with new values.
        
        Parameters:
        - kwargs: Key-value pairs to update
        """
    
    @property
    def key(self) -> StorageKey:
        """Get the storage key for this context"""

Storage Backends

Different storage implementations for state persistence.

class BaseStorage:
    """Abstract base class for FSM storage"""
    
    async def set_state(self, key: StorageKey, state: str | None = None) -> None:
        """Set state for the given key"""
    
    async def get_state(self, key: StorageKey) -> str | None:
        """Get state for the given key"""
    
    async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None:
        """Set data for the given key"""
    
    async def get_data(self, key: StorageKey) -> dict[str, Any]:
        """Get data for the given key"""
    
    async def close(self) -> None:
        """Close the storage connection"""

class MemoryStorage(BaseStorage):
    """In-memory storage (default, not persistent)"""
    
    def __init__(self):
        """Initialize memory storage"""

class RedisStorage(BaseStorage):
    """Redis-based storage for persistence across restarts"""
    
    def __init__(
        self,
        redis: Redis,
        key_builder: KeyBuilder | None = None,
        state_ttl: int | None = None,
        data_ttl: int | None = None
    ):
        """
        Initialize Redis storage.
        
        Parameters:
        - redis: Redis connection instance
        - key_builder: Custom key builder for Redis keys
        - state_ttl: TTL for state keys (seconds)
        - data_ttl: TTL for data keys (seconds)
        """

class StorageKey:
    """Key for identifying user/chat in storage"""
    
    def __init__(
        self,
        bot_id: int,
        chat_id: int,
        user_id: int
    ):
        """
        Initialize storage key.
        
        Parameters:
        - bot_id: Bot identifier
        - chat_id: Chat identifier  
        - user_id: User identifier
        """
    
    @property
    def bot_id(self) -> int:
        """Bot ID"""
    
    @property
    def chat_id(self) -> int:
        """Chat ID"""
    
    @property
    def user_id(self) -> int:
        """User ID"""

FSM Strategies

Different strategies for FSM context isolation.

class FSMStrategy(str, Enum):
    """FSM isolation strategies"""
    CHAT = "CHAT"  # One context per chat
    USER_IN_CHAT = "USER_IN_CHAT"  # One context per user in each chat
    GLOBAL_USER = "GLOBAL_USER"  # One context per user globally

Usage Examples

Basic State Machine

from aiogram import Router, F
from aiogram.types import Message
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup

router = Router()

# Define states
class Form(StatesGroup):
    name = State()
    age = State()
    email = State()

# Start the form
@router.message(Command("form"))
async def start_form(message: Message, state: FSMContext):
    await state.set_state(Form.name)
    await message.answer("What's your name?")

# Handle name input
@router.message(StateFilter(Form.name), F.text)
async def process_name(message: Message, state: FSMContext):
    # Save the name
    await state.update_data(name=message.text)
    
    # Move to next state
    await state.set_state(Form.age)
    await message.answer("What's your age?")

# Handle age input with validation
@router.message(StateFilter(Form.age), F.text.regexp(r"^\d+$"))
async def process_age(message: Message, state: FSMContext):
    age = int(message.text)
    
    if 13 <= age <= 120:
        await state.update_data(age=age)
        await state.set_state(Form.email)
        await message.answer("What's your email?")
    else:
        await message.answer("Please enter a valid age (13-120)")

# Handle invalid age
@router.message(StateFilter(Form.age))
async def invalid_age(message: Message):
    await message.answer("Please enter your age as a number")

# Handle email input
@router.message(StateFilter(Form.email), F.text.regexp(r".+@.+\..+"))
async def process_email(message: Message, state: FSMContext):
    await state.update_data(email=message.text)
    
    # Get all collected data
    data = await state.get_data()
    
    # Clear the state
    await state.clear()
    
    # Show summary
    await message.answer(
        f"Form completed!\n\n"
        f"Name: {data['name']}\n"
        f"Age: {data['age']}\n"
        f"Email: {data['email']}"
    )

# Handle invalid email
@router.message(StateFilter(Form.email))
async def invalid_email(message: Message):
    await message.answer("Please enter a valid email address")

# Cancel command (works in any state)
@router.message(Command("cancel"), StateFilter("*"))
async def cancel_form(message: Message, state: FSMContext):
    current_state = await state.get_state()
    if current_state is None:
        await message.answer("Nothing to cancel")
        return
    
    await state.clear()
    await message.answer("Form cancelled")

Advanced State Machine with Branching

class Survey(StatesGroup):
    # Initial questions
    name = State()
    age = State()
    
    # Branching based on age
    minor_guardian = State()  # For users under 18
    adult_occupation = State()  # For adults
    
    # Common final states
    feedback = State()
    confirmation = State()

@router.message(Command("survey"))
async def start_survey(message: Message, state: FSMContext):
    await state.set_state(Survey.name)
    await message.answer("Welcome to our survey! What's your name?")

@router.message(StateFilter(Survey.name), F.text)
async def process_survey_name(message: Message, state: FSMContext):
    await state.update_data(name=message.text)
    await state.set_state(Survey.age)
    await message.answer("What's your age?")

@router.message(StateFilter(Survey.age), F.text.regexp(r"^\d+$"))
async def process_survey_age(message: Message, state: FSMContext):
    age = int(message.text)
    await state.update_data(age=age)
    
    # Branch based on age
    if age < 18:
        await state.set_state(Survey.minor_guardian)
        await message.answer("Since you're under 18, we need your guardian's name:")
    else:
        await state.set_state(Survey.adult_occupation)
        await message.answer("What's your occupation?")

@router.message(StateFilter(Survey.minor_guardian), F.text)
async def process_guardian(message: Message, state: FSMContext):
    await state.update_data(guardian=message.text)
    await state.set_state(Survey.feedback)
    await message.answer("Any feedback about our service?")

@router.message(StateFilter(Survey.adult_occupation), F.text)
async def process_occupation(message: Message, state: FSMContext):
    await state.update_data(occupation=message.text)
    await state.set_state(Survey.feedback)
    await message.answer("Any feedback about our service?")

@router.message(StateFilter(Survey.feedback), F.text)
async def process_feedback(message: Message, state: FSMContext):
    await state.update_data(feedback=message.text)
    
    # Show summary based on collected data
    data = await state.get_data()
    summary = f"Survey Summary:\nName: {data['name']}\nAge: {data['age']}\n"
    
    if 'guardian' in data:
        summary += f"Guardian: {data['guardian']}\n"
    if 'occupation' in data:
        summary += f"Occupation: {data['occupation']}\n"
    
    summary += f"Feedback: {data['feedback']}\n\nIs this correct? (yes/no)"
    
    await state.set_state(Survey.confirmation)
    await message.answer(summary)

@router.message(StateFilter(Survey.confirmation), F.text.lower().in_(["yes", "y"]))
async def confirm_survey(message: Message, state: FSMContext):
    data = await state.get_data()
    await state.clear()
    
    # Here you would typically save the data
    await message.answer("Thank you! Your survey has been submitted.")

@router.message(StateFilter(Survey.confirmation), F.text.lower().in_(["no", "n"]))
async def reject_survey(message: Message, state: FSMContext):
    await state.clear()
    await message.answer("Survey cancelled. You can start over with /survey")

@router.message(StateFilter(Survey.confirmation))
async def invalid_confirmation(message: Message):
    await message.answer("Please answer 'yes' or 'no'")

State Machine with Inline Keyboards

from aiogram.types import CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder

class Order(StatesGroup):
    category = State()
    item = State()
    quantity = State()
    confirmation = State()

# Product data
CATEGORIES = {
    "food": ["Pizza", "Burger", "Salad"],
    "drinks": ["Coffee", "Tea", "Juice"],
    "desserts": ["Cake", "Ice Cream", "Cookies"]
}

@router.message(Command("order"))
async def start_order(message: Message, state: FSMContext):
    await state.set_state(Order.category)
    
    builder = InlineKeyboardBuilder()
    for category in CATEGORIES.keys():
        builder.button(text=category.title(), callback_data=f"cat_{category}")
    builder.adjust(2)
    
    await message.answer("Choose a category:", reply_markup=builder.as_markup())

@router.callback_query(StateFilter(Order.category), F.data.startswith("cat_"))
async def process_category(callback: CallbackQuery, state: FSMContext):
    category = callback.data.split("_")[1]
    await state.update_data(category=category)
    await state.set_state(Order.item)
    
    builder = InlineKeyboardBuilder()
    for item in CATEGORIES[category]:
        builder.button(text=item, callback_data=f"item_{item.lower().replace(' ', '_')}")
    builder.adjust(1)
    
    await callback.message.edit_text(
        f"Choose an item from {category}:",
        reply_markup=builder.as_markup()
    )

@router.callback_query(StateFilter(Order.item), F.data.startswith("item_"))
async def process_item(callback: CallbackQuery, state: FSMContext):
    item = callback.data.split("_")[1].replace("_", " ").title()
    await state.update_data(item=item)
    await state.set_state(Order.quantity)
    
    builder = InlineKeyboardBuilder()
    for i in range(1, 6):
        builder.button(text=str(i), callback_data=f"qty_{i}")
    builder.adjust(5)
    
    await callback.message.edit_text(
        f"How many {item} would you like?",
        reply_markup=builder.as_markup()
    )

@router.callback_query(StateFilter(Order.quantity), F.data.startswith("qty_"))
async def process_quantity(callback: CallbackQuery, state: FSMContext):
    quantity = int(callback.data.split("_")[1])
    await state.update_data(quantity=quantity)
    
    data = await state.get_data()
    
    builder = InlineKeyboardBuilder()
    builder.button(text="✅ Confirm", callback_data="confirm_order")
    builder.button(text="❌ Cancel", callback_data="cancel_order")
    builder.adjust(1)
    
    await callback.message.edit_text(
        f"Order Summary:\n"
        f"Category: {data['category'].title()}\n"
        f"Item: {data['item']}\n"
        f"Quantity: {quantity}\n\n"
        f"Confirm your order?",
        reply_markup=builder.as_markup()
    )
    await state.set_state(Order.confirmation)

@router.callback_query(StateFilter(Order.confirmation), F.data == "confirm_order")
async def confirm_order(callback: CallbackQuery, state: FSMContext):
    data = await state.get_data()
    await state.clear()
    
    await callback.message.edit_text(
        f"✅ Order confirmed!\n\n"
        f"You ordered {data['quantity']} {data['item']} from {data['category']}.\n"
        f"Your order is being prepared."
    )

@router.callback_query(StateFilter(Order.confirmation), F.data == "cancel_order")
async def cancel_order(callback: CallbackQuery, state: FSMContext):
    await state.clear()
    await callback.message.edit_text("❌ Order cancelled.")

Custom Storage Configuration

import redis.asyncio as redis
from aiogram.fsm.storage.redis import RedisStorage

# Configure Redis storage
redis_client = redis.Redis(host='localhost', port=6379, db=0)
storage = RedisStorage(redis_client, state_ttl=3600, data_ttl=3600)

# Create dispatcher with custom storage
dp = Dispatcher(storage=storage)

# Custom storage with different FSM strategy
dp = Dispatcher(
    storage=storage,
    fsm_strategy=FSMStrategy.GLOBAL_USER  # One context per user globally
)

State Machine with File Upload

class FileUpload(StatesGroup):
    waiting_file = State()
    waiting_description = State()

@router.message(Command("upload"))
async def start_upload(message: Message, state: FSMContext):
    await state.set_state(FileUpload.waiting_file)
    await message.answer("Please send a file (photo, document, or video)")

@router.message(StateFilter(FileUpload.waiting_file), F.content_type.in_({"photo", "document", "video"}))
async def process_file(message: Message, state: FSMContext):
    # Store file information
    if message.photo:
        file_id = message.photo[-1].file_id
        file_type = "photo"
    elif message.document:
        file_id = message.document.file_id
        file_type = "document"
    elif message.video:
        file_id = message.video.file_id
        file_type = "video"
    
    await state.update_data(file_id=file_id, file_type=file_type)
    await state.set_state(FileUpload.waiting_description)
    await message.answer("File received! Please provide a description:")

@router.message(StateFilter(FileUpload.waiting_description), F.text)
async def process_description(message: Message, state: FSMContext):
    await state.update_data(description=message.text)
    data = await state.get_data()
    await state.clear()
    
    # Here you would save the file and description
    await message.answer(
        f"Upload complete!\n"
        f"File type: {data['file_type']}\n"
        f"Description: {data['description']}\n"
        f"File ID: {data['file_id']}"
    )

@router.message(StateFilter(FileUpload.waiting_file))
async def invalid_file(message: Message):
    await message.answer("Please send a photo, document, or video file")

Types

Storage Types

class KeyBuilder:
    """Builder for Redis storage keys"""
    
    def build(self, key: StorageKey, part: str) -> str:
        """Build a Redis key for the given storage key and part"""

class BaseEventIsolation:
    """Base class for event isolation mechanisms"""
    pass

Install with Tessl CLI

npx tessl i tessl/pypi-aiogram

docs

api-methods.md

bot-and-dispatcher.md

filters-and-handlers.md

index.md

state-management.md

types-and-objects.md

utilities-and-enums.md

tile.json