Контекст и состояния¶
MemoryContext¶
Встроенная система состояний для диалогов. Контекст автоматически передается в обработчики:
from maxapi.context import MemoryContext, StatesGroup, State
from maxapi.types import MessageCreated, Command
class Form(StatesGroup):
name = State()
age = State()
@dp.message_created(Command("start"))
async def start_handler(event: MessageCreated, context: MemoryContext):
await context.set_state(Form.name)
await event.message.answer("Как вас зовут?")
@dp.message_created(Form.name)
async def name_handler(event: MessageCreated, context: MemoryContext):
await context.update_data(name=event.message.body.text)
await context.set_state(Form.age)
await event.message.answer("Сколько вам лет?")
@dp.message_created(Form.age)
async def age_handler(event: MessageCreated, context: MemoryContext):
data = await context.get_data()
await event.message.answer(
f"Приятно познакомиться, {data['name']}! "
f"Вам {event.message.body.text} лет."
)
await context.set_state(None) # Сброс состояния
Методы MemoryContext¶
set_state(state)— установить состояние (State или None для сброса)get_state()— получить текущее состояниеget_data()— получить все данные контекстаupdate_data(**kwargs)— обновить данные и вернуть актуальный словарьset_data(data)— полностью заменить данныеclear()— очистить контекст и сбросить состояние
Получение контекста вне хендлера¶
Для простых операций можно не получать объект контекста вручную:
await dp.fsm.set_state(
chat_id=chat_id,
user_id=user_id,
state=Form.name,
)
await dp.fsm.update_data(
chat_id=chat_id,
user_id=user_id,
name="Макс",
)
await dp.fsm.update_data(
chat_id=chat_id,
user_id=user_id,
data={"chat_id": "значение в данных"},
)
data = await dp.fsm.get_data(chat_id=chat_id, user_id=user_id)
await dp.fsm.clear(chat_id=chat_id, user_id=user_id)
Метод использует то же хранилище, TTL и LRU-кеш, что и обычная
обработка событий. FSM manager доступен через Dispatcher: используйте
dp.fsm, а не router.fsm.
TTL для контекста¶
Для автоматической очистки неактивных контекстов можно передать ttl
в секундах. TTL продлевается при каждом чтении или изменении контекста.
Если время истекло, state и data будут лениво сброшены при следующем
обращении.
from maxapi import Dispatcher
from maxapi.context import MemoryContext
dp = Dispatcher(storage=MemoryContext, ttl=1800)
Тот же параметр можно использовать и для RedisContext:
from maxapi.context import RedisContext
dp = Dispatcher(
storage=RedisContext,
redis_client=redis_client,
key_prefix="my_bot",
ttl=1800,
)
StatesGroup¶
Группа состояний для FSM:
class Form(StatesGroup):
name = State() # Автоматически получит имя 'Form:name'
age = State() # Автоматически получит имя 'Form:age'
Фильтрация по состояниям¶
Вы можете ограничивать выполнение хендлеров определенными состояниями:
# Только в состоянии Form.name
@dp.message_created(Form.name)
async def name_handler(event: MessageCreated, context: MemoryContext): ...
# Только когда НЕТ активного состояния
@dp.message_created(None)
async def no_state_handler(event: MessageCreated): ...
# В любом из перечисленных состояний
@dp.message_created(Form.name, Form.age)
async def multi_state_handler(event: MessageCreated): ...
Хранение в Redis¶
Для сохранения состояний и данных между перезапусками бота можно использовать Redis.
Установка зависимостей¶
Пример использования¶
import redis.asyncio as redis
from maxapi import Dispatcher
from maxapi.context import RedisContext
# Инициализация клиента Redis
redis_client = redis.Redis(host="localhost", port=6379, db=0)
# Передача RedisContext в Диспетчер
dp = Dispatcher(
storage=RedisContext,
redis_client=redis_client,
key_prefix="my_bot",
)
RedisContext автоматически сериализует данные в JSON и поддерживает атомарные обновления через Lua-скрипты.
Маркер обновлений (get_updates)¶
В MAX API у get_updates есть маркер обновлений — это внутренняя “позиция” в ленте событий (по сути, пагинация). Если запускать бота без маркера, API может начать отдавать старые обновления (с “начального” маркера), и бот будет повторно обрабатывать прошлые события.
Решение простое: сохраняйте маркер и передавайте его в бота при старте.
Ниже пример хранения маркера в Redis (асинхронный клиент), ровно как ключ bot:marker:
import redis.asyncio as redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
async def load_marker() -> str | None:
return await r.get("bot:marker")
async def save_marker(marker: str) -> None:
await r.set("bot:marker", marker)
Пример простого глобального middleware, который сохраняет текущий маркер в Redis после обработки события:
from maxapi.filters.middleware import BaseMiddleware
from maxapi.types import UpdateUnion
from typing import Any, Awaitable, Callable
class SaveMarkerMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[UpdateUnion, dict[str, Any]], Awaitable[Any]],
event_object: UpdateUnion,
data: dict[str, Any],
) -> Any:
result = await handler(event_object, data)
marker: int | None = event_object.bot.marker_updates
if marker is not None:
await save_marker(str(marker))
return result
Использование (при старте загрузили маркер, установили через set_marker_updates, подключили middleware):
- при запуске загрузить маркер и установить через
bot.set_marker_updates(...), например вasync main():
import asyncio
from maxapi import Bot, Dispatcher
bot = Bot()
dp = Dispatcher()
dp.register_outer_middleware(SaveMarkerMiddleware())
async def main() -> None:
marker = await load_marker() # str | None
if marker is not None:
bot.set_marker_updates(int(marker))
await dp.start_polling(bot)
asyncio.run(main())
- во время работы middleware будет обновлять сохранённый маркер на основании
event_object.bot.marker_updates.
Какой именно маркер видит middleware
bot.marker_updates сдвигается после того, как пачка событий
полностью разобрана и передана в обработчики. Middleware выполняется
во время обработки пачки, то есть видит маркер предыдущей,
уже зафиксированной пачки — не той, событие которой обрабатывает
прямо сейчас.
Это сделано намеренно: маркер, сохранённый до завершения пачки, означал бы, что при падении посреди обработки её события уже считаются доставленными и API больше их не отдаст. Поэтому персистентность здесь — at-least-once: после перезапуска незавершённая пачка приедет заново, и её события обработаются повторно.
Практические следствия:
- обработчики должны быть идемпотентны — повторная обработка события после перезапуска штатна, а не аварийна;
- маркер самой последней пачки в Redis не попадёт: после неё уже не приходит событий, которые запустили бы middleware. После перезапуска эта пачка будет обработана ещё раз.
Если повторная обработка недопустима, сохраняйте не маркер, а признак обработки конкретного события (по его идентификатору) и проверяйте его в начале обработчика.
Изоляция событий (защита от гонок FSM)¶
При параллельной обработке событий (Dispatcher(use_create_task=True) или вебхук)
два быстрых сообщения одного пользователя могут прочитать один и тот же снимок
FSM-состояния до того, как первый хендлер успеет его сбросить. В результате
одноразовый шаг FSM (например, «введите сумму перевода») выполнится дважды.
Механизм изоляции сериализует обработку апдейтов одного пользователя: пока не
завершился предыдущий handle() для ключа (chat_id, user_id), следующий ждёт.
События разных пользователей обрабатываются параллельно, как и раньше.
По умолчанию изоляция отключена (как в aiogram). Включение:
from maxapi import Dispatcher
from maxapi.context import SimpleEventIsolation
dp = Dispatcher(
use_create_task=True,
event_isolation=SimpleEventIsolation(),
)
Ключ изоляции совпадает с ключом FSM-контекста — (chat_id, user_id) из
event.get_ids(). События, у которых часть идентификаторов отсутствует
(например, апдейты без user_id в групповых чатах), делят один контекст —
и сериализуются вместе, ровно на той гранулярности, на которой они делят
состояние.
Изоляция при нескольких процессах¶
SimpleEventIsolation работает в пределах одного процесса. Если бот запущен в
нескольких процессах или инстансах (например, вебхук за балансировщиком),
используйте RedisEventIsolation в паре с RedisContext:
import redis.asyncio as redis
from maxapi import Dispatcher
from maxapi.context import RedisContext, RedisEventIsolation
redis_client = redis.Redis(host="localhost", port=6379, db=0)
dp = Dispatcher(
storage=RedisContext,
redis_client=redis_client,
key_prefix="my_bot",
event_isolation=RedisEventIsolation(
redis_client,
key_prefix="my_bot",
),
)
Долгие хендлеры
Блокировка удерживается на всё время обработки события. Если хендлер внутри
себя ожидает следующее событие того же пользователя, при включённой
изоляции это приведёт к взаимной блокировке до конца хендлера
(у RedisEventIsolation — до lock_timeout, по умолчанию 60 секунд).