# aiogram-ui-router

Declarative UI routing framework for [aiogram 3](https://github.com/aiogram/aiogram). Define complex Telegram bot interfaces through Pydantic schemas instead of writing handler code.

## Features

- **Declarative scenes** — describe screens, keyboards, content and navigation as data
- **Scene navigation** — tree-based navigation with history, back button support
- **Dynamic content** — text templates with flags, variables and conditional rendering
- **Inline & reply keyboards** — static and dynamic keyboards with pagination
- **Rule engine** — declarative conditions for handlers and content
- **Variables** — bot/user/chat-scoped state with typed values and TTL
- **Events** — internal pub/sub event bus with scheduled events (cron, interval)
- **Localization** — multi-language support via Mozilla Fluent
- **Global handlers** — priority-based handlers that work across all scenes
- **Input validation** — custom validators for user text input
- **Analytics** — track scene views, button clicks, navigation flows
- **Schema validation** — detect broken references, unreachable scenes, missing functions
- **Hot reload** — update UI schema without restarting the bot
- **Multi-router** — multiple independent UI routers per bot

## Installation

```bash
pip install aiogram-ui-router
```

With structured logging:

```bash
pip install aiogram-ui-router[logging]
```

## Quick Start

```python
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Message

from ui_router import (
    UIRouter, UIRouterExecutor, Scene, Handler, ActionInstruction,
    ActionType, EventType, TransitionType, MessageContent,
    Keyboard, Button, Flag, FlagGetter, DynamicContent, ContentTemplate,
    SharedServices, InMemoryNavigationStorage, InMemoryVariableRepository,
    EventBus, InMemoryEventScheduler,
)


async def get_user_name(context) -> str:
    return context.event_data["event_from_user"].first_name


schema = UIRouter(
    name="my_bot",
    version="1.0.0",
    initial_scene="welcome",
    scenes=[
        Scene(
            id="welcome",
            name="Welcome",
            flags=[
                Flag(
                    name="name",
                    getter=FlagGetter(type="function", function="get_user_name"),
                ),
            ],
            default_content=MessageContent(
                text=DynamicContent(
                    type="template",
                    template=ContentTemplate(
                        template="Hello, {name}!",
                        flags=["name"],
                    ),
                ),
            ),
            default_keyboard=Keyboard(
                is_inline=True,
                buttons=[
                    [Button(text="About", callback_action="goto_about")],
                ],
            ),
            handlers=[
                Handler(
                    name="goto_about",
                    event_type=EventType.CALLBACK,
                    actions=[
                        ActionInstruction(
                            type=ActionType.GOTO_SCENE,
                            scene_id="about",
                            transition=TransitionType.EDIT_SMART,
                        ),
                    ],
                ),
            ],
        ),
        Scene(
            id="about",
            name="About",
            default_content=MessageContent(text="Built with aiogram-ui-router"),
            default_keyboard=Keyboard(
                buttons=[[Button(text="Back", callback_action="go_back")]],
            ),
            handlers=[
                Handler(
                    name="go_back",
                    event_type=EventType.CALLBACK,
                    actions=[ActionInstruction(type=ActionType.BACK)],
                ),
            ],
        ),
    ],
)


async def main() -> None:
    bot = Bot(token="YOUR_TOKEN")
    dp = Dispatcher(storage=MemoryStorage())

    shared = SharedServices(
        navigation_storage=InMemoryNavigationStorage(),
        variable_repository=InMemoryVariableRepository(),
        event_bus=EventBus(),
        event_scheduler=InMemoryEventScheduler(event_callback=lambda e: None),
    )

    executor = UIRouterExecutor(schema=schema, shared=shared)
    executor.registry.getters.register("get_user_name", get_user_name)

    dp.include_router(executor.get_router())

    @dp.message(CommandStart())
    async def start(message: Message, **kwargs) -> None:
        await executor.start(message.from_user.id, message, **kwargs)

    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())
```

## Core Concepts

### Scenes

A scene is a screen in your bot UI. Each scene has default content, a keyboard, flags (dynamic data) and handlers (reactions to user actions).

### Handlers

Handlers respond to events within a scene. Each handler has a list of sequential actions: navigate to another scene, send/edit messages, save input, call business logic, emit events, etc.

### Flags

Flags provide dynamic data for content templates. They can be static values, async function results, FSM state, user input, variables, or conditional (rule-based).

### Actions

Over 20 action types: `GOTO_SCENE`, `BACK`, `SEND_MESSAGE`, `EDIT_MESSAGE`, `DELETE_MESSAGE`, `SAVE_INPUT`, `SET_VARIABLE`, `BUSINESS_ACTION`, `SCHEDULE_EVENT`, `EMIT_EVENT`, `ANSWER_CALLBACK`, `ANSWER_INLINE_QUERY`, `CUSTOM`, and more.

### Variables

Typed key-value storage scoped to bot, user or chat. Supports `int`, `float`, `bool`, `str`, `datetime`, `json` types with optional TTL and default values.

### Rule Engine

Declarative condition evaluation using operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `contains`, `starts_with`, `ends_with`. Used for conditional content, flags and handler filtering.

## Examples

See the [`examples/`](examples/) directory:

| Example | Description |
|---------|-------------|
| `example.py` | Basic setup with scenes, flags and navigation |
| `example_advanced.py` | Shop with cart, forms, validation and business logic |

## Requirements

- Python >= 3.13
- aiogram >= 3.25

## License

[MIT](LICENSE)

---

# aiogram-ui-router (RU)

Декларативный UI-фреймворк для [aiogram 3](https://github.com/aiogram/aiogram). Описывайте интерфейс Telegram-бота через Pydantic-схемы вместо написания хендлеров вручную.

## Возможности

- **Декларативные сцены** — экраны, клавиатуры, контент и навигация описываются как данные
- **Навигация** — древовидная навигация с историей и кнопкой «Назад»
- **Динамический контент** — шаблоны с флагами, переменными и условным рендерингом
- **Клавиатуры** — inline и reply, статические и динамические с пагинацией
- **Движок правил** — декларативные условия для хендлеров и контента
- **Переменные** — типизированное состояние на уровне бота/пользователя/чата с TTL
- **События** — внутренняя шина событий с планировщиком (cron, interval)
- **Локализация** — мультиязычность через Mozilla Fluent
- **Глобальные хендлеры** — обработчики с приоритетами, работающие во всех сценах
- **Валидация ввода** — кастомные валидаторы для пользовательского текста
- **Аналитика** — отслеживание просмотров сцен, кликов, навигации
- **Валидация схемы** — обнаружение битых ссылок, недостижимых сцен, отсутствующих функций
- **Горячая перезагрузка** — обновление UI-схемы без перезапуска бота
- **Мульти-роутер** — несколько независимых UI-роутеров на одного бота

## Установка

```bash
pip install aiogram-ui-router
```

Со структурированным логированием:

```bash
pip install aiogram-ui-router[logging]
```

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

```python
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Message

from ui_router import (
    UIRouter, UIRouterExecutor, Scene, Handler, ActionInstruction,
    ActionType, EventType, TransitionType, MessageContent,
    Keyboard, Button, Flag, FlagGetter, DynamicContent, ContentTemplate,
    SharedServices, InMemoryNavigationStorage, InMemoryVariableRepository,
    EventBus, InMemoryEventScheduler,
)


async def get_user_name(context) -> str:
    return context.event_data["event_from_user"].first_name


schema = UIRouter(
    name="my_bot",
    version="1.0.0",
    initial_scene="welcome",
    scenes=[
        Scene(
            id="welcome",
            name="Приветствие",
            flags=[
                Flag(
                    name="name",
                    getter=FlagGetter(type="function", function="get_user_name"),
                ),
            ],
            default_content=MessageContent(
                text=DynamicContent(
                    type="template",
                    template=ContentTemplate(
                        template="Привет, {name}!",
                        flags=["name"],
                    ),
                ),
            ),
            default_keyboard=Keyboard(
                is_inline=True,
                buttons=[
                    [Button(text="О боте", callback_action="goto_about")],
                ],
            ),
            handlers=[
                Handler(
                    name="goto_about",
                    event_type=EventType.CALLBACK,
                    actions=[
                        ActionInstruction(
                            type=ActionType.GOTO_SCENE,
                            scene_id="about",
                            transition=TransitionType.EDIT_SMART,
                        ),
                    ],
                ),
            ],
        ),
        Scene(
            id="about",
            name="О боте",
            default_content=MessageContent(text="Собран с помощью aiogram-ui-router"),
            default_keyboard=Keyboard(
                buttons=[[Button(text="Назад", callback_action="go_back")]],
            ),
            handlers=[
                Handler(
                    name="go_back",
                    event_type=EventType.CALLBACK,
                    actions=[ActionInstruction(type=ActionType.BACK)],
                ),
            ],
        ),
    ],
)


async def main() -> None:
    bot = Bot(token="YOUR_TOKEN")
    dp = Dispatcher(storage=MemoryStorage())

    shared = SharedServices(
        navigation_storage=InMemoryNavigationStorage(),
        variable_repository=InMemoryVariableRepository(),
        event_bus=EventBus(),
        event_scheduler=InMemoryEventScheduler(event_callback=lambda e: None),
    )

    executor = UIRouterExecutor(schema=schema, shared=shared)
    executor.registry.getters.register("get_user_name", get_user_name)

    dp.include_router(executor.get_router())

    @dp.message(CommandStart())
    async def start(message: Message, **kwargs) -> None:
        await executor.start(message.from_user.id, message, **kwargs)

    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())
```

## Примеры

Смотрите директорию [`examples/`](examples/):

| Пример | Описание |
|--------|----------|
| `example.py` | Базовый бот: сцены, флаги, навигация |
| `example_advanced.py` | Магазин с корзиной, формами, валидацией и бизнес-логикой |
