diff --git a/.serena/memories/dispatcher/adding_new_event_types.md b/.serena/memories/dispatcher/adding_new_event_types.md new file mode 100644 index 00000000..1196bf13 --- /dev/null +++ b/.serena/memories/dispatcher/adding_new_event_types.md @@ -0,0 +1,43 @@ +# Adding a New Update/Event Type to aiogram Dispatcher + +When Telegram Bot API adds a new update type (e.g. `managed_bot`, `purchased_paid_media`), the following files must be touched. Types, enums, and butcher configs are generated — the dispatcher integration is manual. + +## Checklist (in order) + +### Generated / butcher layer (run `butcher parse && butcher refresh && butcher apply all`) +- `.butcher/types//entity.json` — type definition +- `.butcher/types/Update/entity.json` — add field to Update entity +- `aiogram/types/.py` — generated type class +- `aiogram/types/__init__.py` — export +- `aiogram/enums/update_type.py` — `NEW_TYPE = "new_type"` enum member +- `aiogram/types/update.py` — `new_type: NewType | None = None` field + TYPE_CHECKING import + `__init__` signature + +### Manual dispatcher integration (NOT generated) + +1. **`aiogram/types/update.py`** — `event_type` property (lines ~161-215): add `if self.new_type: return "new_type"` before the `raise UpdateTypeLookupError` line + +2. **`aiogram/dispatcher/router.py`** — two places in `Router.__init__`: + - Add `self.new_type = TelegramEventObserver(router=self, event_name="new_type")` after the last observer attribute (before `self.errors`) + - Add `"new_type": self.new_type,` to the `self.observers` dict (before `"error"`) + +3. **`aiogram/dispatcher/middlewares/user_context.py`** — `resolve_event_context()` method: add `if event.new_type: return EventContext(user=..., chat=...)` before `return EventContext()`. Use `user` field for user-scoped events, `chat` for chat-scoped. No `business_connection_id` unless the event has one. + +### Tests (manual) + +4. **`tests/test_dispatcher/test_dispatcher.py`** — add `pytest.param("new_type", Update(update_id=42, new_type=NewType(...)), has_chat, has_user)` to `test_listen_update` parametrize list. Import `NewType` in the imports block. + +5. **`tests/test_dispatcher/test_router.py`** — add `assert router.observers["new_type"] == router.new_type` to `test_observers_config` + +### Docs (generated or manual stub) +- `docs/api/types/.rst` — RST stub +- `docs/api/types/index.rst` — add to index + +## Key invariants +- The snake_case name must be identical across: `UpdateType` enum value, `Update` field name, `event_type` return string, Router attribute name, observers dict key, and `TelegramEventObserver(event_name=...)`. +- `Update.event_type` uses `@lru_cache()` — never mutate Update fields after construction. +- The routing machinery (`propagate_event`, middleware chains, sub-router propagation) requires **zero changes** — it operates on observer names looked up dynamically. + +## Example: `managed_bot` (API 9.6) +- Type: `ManagedBotUpdated` with fields `user: User` (creator) and `bot_user: User` (the managed bot) +- user_context: `EventContext(user=event.managed_bot.user)` — user only, no chat +- `has_chat=False, has_user=True` in test parametrization diff --git a/aiogram/dispatcher/middlewares/user_context.py b/aiogram/dispatcher/middlewares/user_context.py index 844ddd96..40b407e9 100644 --- a/aiogram/dispatcher/middlewares/user_context.py +++ b/aiogram/dispatcher/middlewares/user_context.py @@ -183,4 +183,6 @@ class UserContextMiddleware(BaseMiddleware): return EventContext( user=event.purchased_paid_media.from_user, ) + if event.managed_bot: + return EventContext(user=event.managed_bot.user) return EventContext() diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 90a2362a..532018d5 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -85,6 +85,7 @@ class Router: router=self, event_name="purchased_paid_media", ) + self.managed_bot = TelegramEventObserver(router=self, event_name="managed_bot") self.errors = self.error = TelegramEventObserver(router=self, event_name="error") @@ -115,6 +116,7 @@ class Router: "edited_business_message": self.edited_business_message, "business_message": self.business_message, "purchased_paid_media": self.purchased_paid_media, + "managed_bot": self.managed_bot, "error": self.errors, } diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 6c964a89..cf13b22a 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -211,6 +211,8 @@ class Update(TelegramObject): return "business_message" if self.purchased_paid_media: return "purchased_paid_media" + if self.managed_bot: + return "managed_bot" raise UpdateTypeLookupError("Update does not contain any known event type.") diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py index c4899a87..014f44d3 100644 --- a/tests/test_dispatcher/test_dispatcher.py +++ b/tests/test_dispatcher/test_dispatcher.py @@ -30,6 +30,7 @@ from aiogram.types import ( ChatMemberUpdated, ChosenInlineResult, InlineQuery, + ManagedBotUpdated, Message, MessageReactionCountUpdated, MessageReactionUpdated, @@ -602,6 +603,18 @@ class TestDispatcher: False, True, ), + pytest.param( + "managed_bot", + Update( + update_id=42, + managed_bot=ManagedBotUpdated( + user=User(id=42, is_bot=False, first_name="Test"), + bot_user=User(id=100, is_bot=True, first_name="ManagedBot"), + ), + ), + False, + True, + ), ], ) async def test_listen_update( diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py index 1ac78480..712786b1 100644 --- a/tests/test_dispatcher/test_router.py +++ b/tests/test_dispatcher/test_router.py @@ -71,6 +71,7 @@ class TestRouter: assert router.observers["shipping_query"] == router.shipping_query assert router.observers["pre_checkout_query"] == router.pre_checkout_query assert router.observers["poll"] == router.poll + assert router.observers["managed_bot"] == router.managed_bot async def test_emit_startup(self): router1 = Router()