Skip to main content

Reusable Telegram bot framework with Clean Architecture

Project description

Bot Framework

Переиспользуемая Python-библиотека для создания ботов на любых платформах с Clean Architecture.

Документация: botframework.smartist.dev

Платформы

Платформа Транспорт Optional dependency
Telegram pyTelegramBotAPI (polling / webhooks) bot-framework[telegram]
Facebook Messenger Webhooks (FastAPI + uvicorn) bot-framework[facebook]
Max (dev.max.ru) Long polling (httpx) bot-framework[max]

Бизнес-логика (flows, roles, phrases) не зависит от платформы — переключение между мессенджерами требует только замены MessageCore.

Установка

pip install bot-framework[all]          # все платформы + postgres + redis
pip install bot-framework[telegram]     # только Telegram
pip install bot-framework[max]          # только Max
pip install bot-framework[facebook]     # только Facebook

Быстрый старт

Telegram

from pathlib import Path
from bot_framework.app import BotApplication

app = BotApplication(
    bot_token="YOUR_BOT_TOKEN",
    database_url="postgres://user:pass@localhost/dbname",
    redis_url="redis://localhost:6379/0",
    roles_json_path=Path("data/roles.json"),
    phrases_json_path=Path("data/phrases.json"),
)

app.run()

Max

from bot_framework.platform.max import MaxMessageCore

core = MaxMessageCore(token="YOUR_MAX_BOT_TOKEN")

# Регистрация handlers — так же, как для Telegram
core.callback_handler_registry.register(my_callback_handler)
core.message_handler_registry.register(my_message_handler, commands=["start"])

core.run()  # запускает long polling

Facebook Messenger

from bot_framework.platform.facebook import FacebookMessageCore

core = FacebookMessageCore(
    page_access_token="YOUR_PAGE_TOKEN",
    verify_token="YOUR_VERIFY_TOKEN",
)

core.callback_handler_registry.register(my_callback_handler)
core.message_handler_registry.register(my_message_handler, commands=["start"])

core.run(host="0.0.0.0", port=8000)  # запускает webhook-сервер

Полный пример: flow с шагами, check_roles и factory

Типичная структура flow:

src/flows/registration_flow/
├── factory.py                          # Сборка зависимостей, создание Flow
├── handlers/
│   ├── start_registration_handler.py   # Запуск flow
│   └── name_input_handler.py           # Приём имени → state → flow.route()
├── steps/
│   ├── ask_name_step.py                # Проверяет state.name → вызывает presenter
│   └── ask_email_step.py               # Проверяет state.email → вызывает presenter
├── presenters/
│   ├── ask_name_presenter.py           # Отправка вопроса "Как вас зовут?"
│   ├── ask_email_presenter.py          # Отправка вопроса "Ваш email?"
│   └── confirm_presenter.py            # Финальное подтверждение
├── entities/
│   └── registration_state.py           # Состояние flow
└── repos/
    └── redis_registration_state_storage.py

Принцип работы

Handler → записывает данные в State → вызывает flow.route(user)
Flow    → итерирует Steps по порядку
Step    → проверяет State → если не заполнено, вызывает Presenter → stop
                           → если заполнено, return True → next step
  • Handler не знает о presenters — только пишет в state и вызывает flow.route()
  • Step проверяет своё поле в state и вызывает presenter при необходимости
  • Flow задаёт порядок шагов и вызывает on_complete когда все шаги пройдены

1. State — состояние flow

# entities/registration_state.py
from pydantic import BaseModel


class RegistrationState(BaseModel):
    user_id: int
    name: str | None = None
    email: str | None = None

2. Steps — шаги flow

Каждый шаг наследует BaseStep. Метод execute() возвращает:

  • True — шаг завершён, перейти к следующему
  • False — шаг отправил сообщение пользователю (через presenter), ждём ответа
# steps/ask_name_step.py
from bot_framework.domain.flow_management.step_flow import BaseStep
from bot_framework import User

from ..presenters import AskNamePresenter
from ..entities import RegistrationState


class AskNameStep(BaseStep[RegistrationState]):
    name = "ask_name"

    def __init__(self, presenter: AskNamePresenter) -> None:
        self._presenter = presenter

    def execute(self, user: User, state: RegistrationState) -> bool:
        if state.name is not None:
            return True  # поле заполнено — следующий шаг
        self._presenter.send(chat_id=user.id, language_code=user.language_code)
        return False  # ждём ввода от пользователя

3. Presenters — отображение

Presenter формирует и отправляет сообщение. Не знает о шагах и handlers:

# presenters/ask_name_presenter.py
from bot_framework import IMessageSender
from bot_framework.domain.language_management.repos.protocols import IPhraseRepo


class AskNamePresenter:
    def __init__(
        self,
        message_sender: IMessageSender,
        phrase_repo: IPhraseRepo,
    ) -> None:
        self._message_sender = message_sender
        self._phrase_repo = phrase_repo

    def send(self, chat_id: int, language_code: str) -> None:
        text = self._phrase_repo.get_phrase(
            key="registration.ask_name",
            language_code=language_code,
        )
        self._message_sender.send(chat_id=chat_id, text=text)

4. Handlers — обработка ввода пользователя

Handler получает данные от пользователя, записывает в state и вызывает flow.route(user). Handler не вызывает presenters и не знает о шагах — только пишет данные и передаёт управление flow.

Каждый handler использует декоратор @check_roles (callback) или @check_message_roles (message).

Message handler (текстовый ввод):

# handlers/name_input_handler.py
from bot_framework import BotMessage, check_message_roles
from bot_framework.domain.flow_management.step_flow import Flow
from bot_framework.domain.role_management.repos.protocols import IRoleRepo, IUserRepo

from ..entities import RegistrationState


class NameInputHandler:
    def __init__(
        self,
        role_repo: IRoleRepo,
        user_repo: IUserRepo,
        state_storage: "IStepStateStorage[RegistrationState]",
    ) -> None:
        self.role_repo = role_repo
        self.allowed_roles: set[str] | None = None  # None = доступно всем
        self._user_repo = user_repo
        self._state_storage = state_storage
        self.flow: Flow[RegistrationState] | None = None

    @check_message_roles
    def handle(self, message: BotMessage) -> None:
        if not message.from_user:
            return

        state = self._state_storage.get(message.from_user.id)
        if state is None:
            return

        # Только записываем данные в state
        state.name = message.text
        self._state_storage.save(state)

        # Передаём управление flow — он сам вызовет нужный step/presenter
        if self.flow:
            user = self._user_repo.get_by_id(message.from_user.id)
            self.flow.route(user)

Callback handler (запуск flow по кнопке):

# handlers/start_registration_handler.py
from uuid import uuid4

from bot_framework import BotCallback, ICallbackAnswerer, check_roles
from bot_framework.domain.flow_management.step_flow import Flow
from bot_framework.domain.role_management.repos.protocols import IRoleRepo, IUserRepo

from ..entities import RegistrationState


class StartRegistrationHandler:
    def __init__(
        self,
        callback_answerer: ICallbackAnswerer,
        role_repo: IRoleRepo,
        user_repo: IUserRepo,
    ) -> None:
        self.callback_answerer = callback_answerer
        self.role_repo = role_repo
        self.allowed_roles: set[str] | None = None
        self._user_repo = user_repo
        self.flow: Flow[RegistrationState] | None = None
        self.prefix = uuid4().hex

    @check_roles
    def handle(self, callback: BotCallback) -> None:
        self.callback_answerer.answer(callback_query_id=callback.id)
        user = self._user_repo.get_by_id(callback.user_id)
        if self.flow:
            self.flow.start(user, source_message=callback.message)

5. Flow — сборка шагов

Flow задаёт порядок шагов и действие по завершении. Внутри flow.route(user) итерирует шаги по порядку — каждый шаг проверяет своё поле в state:

from bot_framework.domain.flow_management.step_flow import Flow

flow = Flow[RegistrationState](
    name="registration",
    state_factory=lambda user_id: RegistrationState(user_id=user_id),
    state_storage=state_storage,
)

flow.add_step(AskNameStep(presenter=ask_name_presenter))    # 1. Имя
flow.add_step(AskEmailStep(presenter=ask_email_presenter))  # 2. Email
flow.on_complete(lambda user, state: confirm_presenter.send(user, state))

6. Factory — сборка всех компонентов

Factory создаёт presenters, steps, flow и handlers. Связывает handlers с flow:

# factory.py
from bot_framework import (
    ICallbackAnswerer,
    ICallbackHandlerRegistry,
    IMessageHandlerRegistry,
    IMessageSender,
)
from bot_framework.domain.language_management.repos.protocols import IPhraseRepo
from bot_framework.domain.role_management.repos.protocols import IRoleRepo, IUserRepo
from bot_framework.domain.flow_management.step_flow import Flow, IStepStateStorage

from .steps import AskNameStep, AskEmailStep
from .handlers import StartRegistrationHandler, NameInputHandler
from .presenters import AskNamePresenter, AskEmailPresenter, ConfirmPresenter
from .entities import RegistrationState


class RegistrationFlowFactory:
    def __init__(
        self,
        callback_answerer: ICallbackAnswerer,
        message_sender: IMessageSender,
        phrase_repo: IPhraseRepo,
        role_repo: IRoleRepo,
        user_repo: IUserRepo,
        state_storage: IStepStateStorage[RegistrationState],
    ) -> None:
        self._callback_answerer = callback_answerer
        self._message_sender = message_sender
        self._phrase_repo = phrase_repo
        self._role_repo = role_repo
        self._user_repo = user_repo
        self._state_storage = state_storage

        self._flow: Flow[RegistrationState] | None = None
        self._start_handler: StartRegistrationHandler | None = None
        self._name_handler: NameInputHandler | None = None

    def _get_flow(self) -> Flow[RegistrationState]:
        if self._flow is not None:
            return self._flow

        ask_name_presenter = AskNamePresenter(
            message_sender=self._message_sender,
            phrase_repo=self._phrase_repo,
        )
        ask_email_presenter = AskEmailPresenter(
            message_sender=self._message_sender,
            phrase_repo=self._phrase_repo,
        )
        confirm_presenter = ConfirmPresenter(
            message_sender=self._message_sender,
            phrase_repo=self._phrase_repo,
        )

        self._flow = Flow[RegistrationState](
            name="registration",
            state_factory=lambda uid: RegistrationState(user_id=uid),
            state_storage=self._state_storage,
        )
        self._flow.add_step(AskNameStep(presenter=ask_name_presenter))
        self._flow.add_step(AskEmailStep(presenter=ask_email_presenter))
        self._flow.on_complete(
            lambda user, state: confirm_presenter.send(user, state)
        )

        return self._flow

    def _get_start_handler(self) -> StartRegistrationHandler:
        if self._start_handler is None:
            self._start_handler = StartRegistrationHandler(
                callback_answerer=self._callback_answerer,
                role_repo=self._role_repo,
                user_repo=self._user_repo,
            )
            self._start_handler.flow = self._get_flow()
        return self._start_handler

    def _get_name_handler(self) -> NameInputHandler:
        if self._name_handler is None:
            self._name_handler = NameInputHandler(
                role_repo=self._role_repo,
                user_repo=self._user_repo,
                state_storage=self._state_storage,
            )
            self._name_handler.flow = self._get_flow()
        return self._name_handler

    def register_handlers(
        self,
        callback_registry: ICallbackHandlerRegistry,
        message_registry: IMessageHandlerRegistry,
    ) -> None:
        callback_registry.register(self._get_start_handler())
        # Регистрация message handlers для текстового ввода
        # message_registry.register(self._get_name_handler(), ...)

7. Подключение к BotApplication

from pathlib import Path
from bot_framework.app import BotApplication

app = BotApplication(
    bot_token="YOUR_BOT_TOKEN",
    database_url="postgres://user:pass@localhost/dbname",
    redis_url="redis://localhost:6379/0",
    phrases_json_path=Path("data/phrases.json"),
)

factory = RegistrationFlowFactory(
    callback_answerer=app.callback_answerer,
    message_sender=app.message_sender,
    phrase_repo=app.phrase_repo,
    role_repo=app.role_repo,
    user_repo=app.user_repo,
    state_storage=RedisRegistrationStateStorage(redis_url="redis://localhost:6379/0"),
)

factory.register_handlers(
    callback_registry=app.callback_handler_registry,
    message_registry=app.core.message_handler_registry,
)

app.run()

Flow Stack

Когда нужен Flow Stack

Один flow — это линейная цепочка шагов: шаг 1 → шаг 2 → шаг 3 → завершение. Каждый шаг проверяет одно поле в state и вызывает один presenter.

Но если на каком-то шаге возникает ответвление — например, на шаге «выбор адреса» пользователь нажимает «Добавить новый адрес», и это требует отдельной цепочки шагов (город → улица → дом → квартира) — линейный flow это не покрывает.

В этом случае ответвление оформляется как отдельный flow, и flow соединяются через Flow Stack:

Registration Flow (шаг 1 → шаг 2 → шаг 3)
                              ↓ push("add_address")
                    Add Address Flow (город → улица → дом)
                              ↓ pop_and_return()
                    ← возврат в Registration Flow на шаг 3

Flow Stack работает как стек вызовов функций: push — входим в дочерний flow, pop_and_return — завершаем его и возвращаемся в родительский.

Правило

  • Один flow = одна линейная цепочка шагов (без ветвлений)
  • Как только появляется ответвление — выносим его в отдельный flow
  • Flow Stack соединяет flow между собой с возможностью возврата

API

from bot_framework.domain.flow_management.services import FlowStackNavigator
from bot_framework.domain.flow_management import FlowRegistry

# Регистрация flow в реестре
registry = FlowRegistry()
registry.register("registration", registration_flow_router)
registry.register("add_address", add_address_flow_router)

# Навигация
navigator = FlowStackNavigator(
    storage=redis_flow_stack_storage,
    registry=registry,
    validator=flow_stack_validator,
)

# Войти в дочерний flow (добавить в стек)
navigator.push(user, "add_address")

# Завершить текущий flow и вернуться к родительскому
navigator.pop_and_return(user)

# Завершить текущий flow без возврата
navigator.terminate(user)

# Очистить весь стек (например, при /start)
navigator.clear_all(user)

Декораторы check_roles

Ограничение доступа к handler по ролям пользователя. Декоратор обязателен для каждого handler.

@check_roles — для callback-обработчиков

from bot_framework import BotCallback, check_roles

class MyHandler:
    def __init__(self, role_repo: IRoleRepo, callback_answerer: ICallbackAnswerer):
        self.role_repo = role_repo                    # обязательно
        self.callback_answerer = callback_answerer     # опционально — показывает alert
        self.allowed_roles: set[str] = {"admin"}       # None = доступно всем

    @check_roles
    def handle(self, callback: BotCallback) -> None:
        ...

@check_message_roles — для message-обработчиков

from bot_framework import BotMessage, check_message_roles

class MyHandler:
    def __init__(self, role_repo: IRoleRepo, message_sender: IMessageSender):
        self.role_repo = role_repo                # обязательно
        self.message_sender = message_sender       # опционально — отправляет ошибку
        self.allowed_roles: set[str] = {"manager"} # None = доступно всем

    @check_message_roles
    def handle(self, message: BotMessage) -> None:
        ...

Конфигурация

Роли (data/roles.json)

{
  "roles": [
    {"name": "admin", "description": "Администратор"},
    {"name": "manager", "description": "Менеджер"}
  ]
}

Фразы (data/phrases.json)

{
  "mybot.greeting": {
    "ru": "Привет!",
    "en": "Hello!"
  }
}

Кнопки главного меню

app.add_main_menu_button("mybot.orders", orders_handler)

Ограничение /start по ролям

app.set_start_allowed_roles({"admin", "manager"})

Протоколы сообщений

Протокол Метод Описание
IMessageSender send() Отправка сообщения
IMessageReplacer replace() Редактирование сообщения
IMessageDeleter delete() Удаление сообщения
IDocumentSender send_document() Отправка файла
ICallbackAnswerer answer() Ответ на callback query

Support Chat (Telegram)

Зеркалирование переписки с пользователем в Telegram-супергруппу с топиками.

app = BotApplication(
    bot_token="YOUR_BOT_TOKEN",
    database_url="postgres://user:pass@localhost/dbname",
    redis_url="redis://localhost:6379/0",
    support_chat_id=-1001234567890,
)

Требования: супергруппа с включёнными Topics, бот — админ с правом Manage Topics.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bot_framework-0.9.1.tar.gz (69.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bot_framework-0.9.1-py3-none-any.whl (150.2 kB view details)

Uploaded Python 3

File details

Details for the file bot_framework-0.9.1.tar.gz.

File metadata

  • Download URL: bot_framework-0.9.1.tar.gz
  • Upload date:
  • Size: 69.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for bot_framework-0.9.1.tar.gz
Algorithm Hash digest
SHA256 be2bb6c8815cd6033c9adf724deb27a6260d246cf9b3c8b00e5c5f08b26a2c74
MD5 0f339129cfceb6beecb877a1e0537694
BLAKE2b-256 0021206b80ede282d7d2e7492c04212b0b9fb52f427869a095a17f2a49ac7de8

See more details on using hashes here.

File details

Details for the file bot_framework-0.9.1-py3-none-any.whl.

File metadata

  • Download URL: bot_framework-0.9.1-py3-none-any.whl
  • Upload date:
  • Size: 150.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for bot_framework-0.9.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6967521f07e7313a093cf662d29916947a786af8abd5bbd01b59167306081b1b
MD5 c738e5b23a23b9cf724b67636b47cae6
BLAKE2b-256 c49dcc5f3b1512ba269928fe89f3f0bcc2c4379447ea50c4e783744e2a5aec53

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page