mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Add support for managed_bot updates
This commit is contained in:
parent
da14db0963
commit
81cc924ed8
6 changed files with 63 additions and 0 deletions
43
.serena/memories/dispatcher/adding_new_event_types.md
Normal file
43
.serena/memories/dispatcher/adding_new_event_types.md
Normal file
|
|
@ -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/<TypeName>/entity.json` — type definition
|
||||||
|
- `.butcher/types/Update/entity.json` — add field to Update entity
|
||||||
|
- `aiogram/types/<type_name>.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/<type_name>.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
|
||||||
|
|
@ -183,4 +183,6 @@ class UserContextMiddleware(BaseMiddleware):
|
||||||
return EventContext(
|
return EventContext(
|
||||||
user=event.purchased_paid_media.from_user,
|
user=event.purchased_paid_media.from_user,
|
||||||
)
|
)
|
||||||
|
if event.managed_bot:
|
||||||
|
return EventContext(user=event.managed_bot.user)
|
||||||
return EventContext()
|
return EventContext()
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ class Router:
|
||||||
router=self,
|
router=self,
|
||||||
event_name="purchased_paid_media",
|
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")
|
self.errors = self.error = TelegramEventObserver(router=self, event_name="error")
|
||||||
|
|
||||||
|
|
@ -115,6 +116,7 @@ class Router:
|
||||||
"edited_business_message": self.edited_business_message,
|
"edited_business_message": self.edited_business_message,
|
||||||
"business_message": self.business_message,
|
"business_message": self.business_message,
|
||||||
"purchased_paid_media": self.purchased_paid_media,
|
"purchased_paid_media": self.purchased_paid_media,
|
||||||
|
"managed_bot": self.managed_bot,
|
||||||
"error": self.errors,
|
"error": self.errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,8 @@ class Update(TelegramObject):
|
||||||
return "business_message"
|
return "business_message"
|
||||||
if self.purchased_paid_media:
|
if self.purchased_paid_media:
|
||||||
return "purchased_paid_media"
|
return "purchased_paid_media"
|
||||||
|
if self.managed_bot:
|
||||||
|
return "managed_bot"
|
||||||
|
|
||||||
raise UpdateTypeLookupError("Update does not contain any known event type.")
|
raise UpdateTypeLookupError("Update does not contain any known event type.")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ from aiogram.types import (
|
||||||
ChatMemberUpdated,
|
ChatMemberUpdated,
|
||||||
ChosenInlineResult,
|
ChosenInlineResult,
|
||||||
InlineQuery,
|
InlineQuery,
|
||||||
|
ManagedBotUpdated,
|
||||||
Message,
|
Message,
|
||||||
MessageReactionCountUpdated,
|
MessageReactionCountUpdated,
|
||||||
MessageReactionUpdated,
|
MessageReactionUpdated,
|
||||||
|
|
@ -602,6 +603,18 @@ class TestDispatcher:
|
||||||
False,
|
False,
|
||||||
True,
|
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(
|
async def test_listen_update(
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ class TestRouter:
|
||||||
assert router.observers["shipping_query"] == router.shipping_query
|
assert router.observers["shipping_query"] == router.shipping_query
|
||||||
assert router.observers["pre_checkout_query"] == router.pre_checkout_query
|
assert router.observers["pre_checkout_query"] == router.pre_checkout_query
|
||||||
assert router.observers["poll"] == router.poll
|
assert router.observers["poll"] == router.poll
|
||||||
|
assert router.observers["managed_bot"] == router.managed_bot
|
||||||
|
|
||||||
async def test_emit_startup(self):
|
async def test_emit_startup(self):
|
||||||
router1 = Router()
|
router1 = Router()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue