Modern and fully asynchronous framework for Telegram Bot API
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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.
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"""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"""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 globallyfrom 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")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'")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.")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
)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")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"""
passInstall with Tessl CLI
npx tessl i tessl/pypi-aiogram