mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
* Fix scene handling for channel updates with missing FSM state (#1743) * Add changelog entry for scene handling fix * Refine scene context error handling
This commit is contained in:
parent
1708980ceb
commit
da7bfdca0c
6 changed files with 298 additions and 4 deletions
1
CHANGES/1743.bugfix.rst
Normal file
1
CHANGES/1743.bugfix.rst
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fixed scene handling for ``channel_post`` and ``edited_channel_post`` when Scenes are registered but FSM state is unavailable, and added channel-scoped FSM context support for ``CHAT``/``CHAT_TOPIC`` strategies.
|
||||
|
|
@ -70,6 +70,9 @@ class FSMContextMiddleware(BaseMiddleware):
|
|||
) -> FSMContext | None:
|
||||
if chat_id is None:
|
||||
chat_id = user_id
|
||||
elif user_id is None and self.strategy in {FSMStrategy.CHAT, FSMStrategy.CHAT_TOPIC}:
|
||||
# CHAT/CHAT_TOPIC are chat-scoped, so missing user_id can fallback to chat_id.
|
||||
user_id = chat_id
|
||||
|
||||
if chat_id is not None and user_id is not None:
|
||||
chat_id, user_id, thread_id = apply_strategy(
|
||||
|
|
|
|||
|
|
@ -248,8 +248,16 @@ class SceneHandlerWrapper:
|
|||
event: TelegramObject,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
try:
|
||||
state: FSMContext = kwargs["state"]
|
||||
scenes: ScenesManager = kwargs["scenes"]
|
||||
except KeyError as error:
|
||||
missing_key = error.args[0]
|
||||
msg = (
|
||||
f"Scene context key {missing_key!r} is not available. "
|
||||
"Ensure FSM is enabled and pipeline is intact."
|
||||
)
|
||||
raise SceneException(msg) from None
|
||||
event_update: Update = kwargs["event_update"]
|
||||
scene = self.scene(
|
||||
wizard=SceneWizard(
|
||||
|
|
@ -768,12 +776,15 @@ class SceneRegistry:
|
|||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
assert isinstance(event, Update), "Event must be an Update instance"
|
||||
state = data.get("state")
|
||||
if state is None:
|
||||
return await handler(event, data)
|
||||
|
||||
data["scenes"] = ScenesManager(
|
||||
registry=self,
|
||||
update_type=event.event_type,
|
||||
event=event.event,
|
||||
state=data["state"],
|
||||
state=state,
|
||||
data=data,
|
||||
)
|
||||
return await handler(event, data)
|
||||
|
|
@ -784,12 +795,16 @@ class SceneRegistry:
|
|||
event: TelegramObject,
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
state = data.get("state")
|
||||
if state is None:
|
||||
return await handler(event, data)
|
||||
|
||||
update: Update = data["event_update"]
|
||||
data["scenes"] = ScenesManager(
|
||||
registry=self,
|
||||
update_type=update.event_type,
|
||||
event=event,
|
||||
state=data["state"],
|
||||
state=state,
|
||||
data=data,
|
||||
)
|
||||
return await handler(event, data)
|
||||
|
|
|
|||
90
tests/test_fsm/test_middleware.py
Normal file
90
tests/test_fsm/test_middleware.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
from aiogram.fsm.middleware import FSMContextMiddleware
|
||||
from aiogram.fsm.storage.memory import DisabledEventIsolation, MemoryStorage
|
||||
from aiogram.fsm.strategy import FSMStrategy
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
CHANNEL_ID = -1001234567890
|
||||
THREAD_ID = 42
|
||||
|
||||
|
||||
def create_middleware(strategy: FSMStrategy) -> FSMContextMiddleware:
|
||||
return FSMContextMiddleware(
|
||||
storage=MemoryStorage(),
|
||||
events_isolation=DisabledEventIsolation(),
|
||||
strategy=strategy,
|
||||
)
|
||||
|
||||
|
||||
class TestFSMContextMiddleware:
|
||||
def test_resolve_context_for_channel_in_chat_strategy(self):
|
||||
bot = MockedBot()
|
||||
middleware = create_middleware(FSMStrategy.CHAT)
|
||||
|
||||
context = middleware.resolve_context(
|
||||
bot=bot,
|
||||
chat_id=CHANNEL_ID,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
assert context is not None
|
||||
assert context.key.chat_id == CHANNEL_ID
|
||||
assert context.key.user_id == CHANNEL_ID
|
||||
|
||||
def test_resolve_context_with_missing_user_in_chat_topic_strategy_uses_chat_id_for_user_id(
|
||||
self,
|
||||
):
|
||||
"""
|
||||
When user_id is absent (e.g., channel-like updates), chat-scoped strategies
|
||||
should still build a stable FSM key by mirroring chat_id into user_id.
|
||||
"""
|
||||
bot = MockedBot()
|
||||
middleware = create_middleware(FSMStrategy.CHAT_TOPIC)
|
||||
|
||||
context = middleware.resolve_context(
|
||||
bot=bot,
|
||||
chat_id=CHANNEL_ID,
|
||||
user_id=None,
|
||||
thread_id=THREAD_ID,
|
||||
)
|
||||
|
||||
assert context is not None
|
||||
assert context.key.chat_id == CHANNEL_ID
|
||||
assert context.key.user_id == CHANNEL_ID
|
||||
assert context.key.thread_id == THREAD_ID
|
||||
|
||||
def test_resolve_context_for_channel_in_user_in_chat_strategy(self):
|
||||
bot = MockedBot()
|
||||
middleware = create_middleware(FSMStrategy.USER_IN_CHAT)
|
||||
|
||||
context = middleware.resolve_context(
|
||||
bot=bot,
|
||||
chat_id=CHANNEL_ID,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
assert context is None
|
||||
|
||||
def test_resolve_context_for_channel_in_global_user_strategy(self):
|
||||
bot = MockedBot()
|
||||
middleware = create_middleware(FSMStrategy.GLOBAL_USER)
|
||||
|
||||
context = middleware.resolve_context(
|
||||
bot=bot,
|
||||
chat_id=CHANNEL_ID,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
assert context is None
|
||||
|
||||
def test_resolve_context_for_channel_in_user_in_topic_strategy(self):
|
||||
bot = MockedBot()
|
||||
middleware = create_middleware(FSMStrategy.USER_IN_TOPIC)
|
||||
|
||||
context = middleware.resolve_context(
|
||||
bot=bot,
|
||||
chat_id=CHANNEL_ID,
|
||||
user_id=None,
|
||||
thread_id=THREAD_ID,
|
||||
)
|
||||
|
||||
assert context is None
|
||||
|
|
@ -309,6 +309,34 @@ class TestSceneHandlerWrapper:
|
|||
# Check whether result is correct
|
||||
assert result == 42
|
||||
|
||||
async def test_scene_handler_wrapper_call_without_scene_context(self):
|
||||
class MyScene(Scene):
|
||||
pass
|
||||
|
||||
async def handler_mock(*args, **kwargs):
|
||||
return 42
|
||||
|
||||
event_update_mock = Update(
|
||||
update_id=42,
|
||||
message=Message(
|
||||
message_id=42,
|
||||
text="test",
|
||||
date=datetime.now(),
|
||||
chat=Chat(
|
||||
type="private",
|
||||
id=42,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
scene_handler_wrapper = SceneHandlerWrapper(MyScene, handler_mock)
|
||||
|
||||
with pytest.raises(
|
||||
SceneException,
|
||||
match="Scene context key 'state' is not available. Ensure FSM is enabled and pipeline is intact.",
|
||||
):
|
||||
await scene_handler_wrapper(event_update_mock, event_update=event_update_mock)
|
||||
|
||||
def test_scene_handler_wrapper_str(self):
|
||||
class MyScene(Scene):
|
||||
pass
|
||||
|
|
@ -1558,6 +1586,27 @@ class TestSceneRegistry:
|
|||
handler.assert_called_once_with(event, data)
|
||||
assert result == handler.return_value
|
||||
|
||||
async def test_scene_registry_update_middleware_without_state(self):
|
||||
router = Router()
|
||||
registry = SceneRegistry(router)
|
||||
handler = AsyncMock(spec=NextMiddlewareType)
|
||||
event = Update(
|
||||
update_id=42,
|
||||
message=Message(
|
||||
message_id=42,
|
||||
text="test",
|
||||
date=datetime.now(),
|
||||
chat=Chat(id=42, type="private"),
|
||||
),
|
||||
)
|
||||
data = {}
|
||||
|
||||
result = await registry._update_middleware(handler, event, data)
|
||||
|
||||
assert "scenes" not in data
|
||||
handler.assert_called_once_with(event, data)
|
||||
assert result == handler.return_value
|
||||
|
||||
async def test_scene_registry_update_middleware_not_update(self, bot: MockedBot):
|
||||
router = Router()
|
||||
registry = SceneRegistry(router)
|
||||
|
|
@ -1604,6 +1653,24 @@ class TestSceneRegistry:
|
|||
handler.assert_called_once_with(event, data)
|
||||
assert result == handler.return_value
|
||||
|
||||
async def test_scene_registry_middleware_without_state(self):
|
||||
router = Router()
|
||||
registry = SceneRegistry(router)
|
||||
handler = AsyncMock(spec=NextMiddlewareType)
|
||||
event = Message(
|
||||
message_id=42,
|
||||
text="test",
|
||||
date=datetime.now(),
|
||||
chat=Chat(id=42, type="private"),
|
||||
)
|
||||
data = {}
|
||||
|
||||
result = await registry._middleware(handler, event, data)
|
||||
|
||||
assert "scenes" not in data
|
||||
handler.assert_called_once_with(event, data)
|
||||
assert result == handler.return_value
|
||||
|
||||
|
||||
class TestSceneInheritance:
|
||||
def test_inherit_handlers(self):
|
||||
|
|
|
|||
118
tests/test_issues/test_1743_channel_post_with_scenes.py
Normal file
118
tests/test_issues/test_1743_channel_post_with_scenes.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram import Dispatcher, F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.scene import Scene, SceneRegistry, on
|
||||
from aiogram.fsm.strategy import FSMStrategy
|
||||
from aiogram.types import Chat, Message, Update
|
||||
from tests.mocked_bot import MockedBot
|
||||
|
||||
CHANNEL_ID = -1001234567890
|
||||
|
||||
|
||||
class BrowseScene(Scene, state="browse"):
|
||||
pass
|
||||
|
||||
|
||||
class ChannelStateScene(Scene, state="channel_state"):
|
||||
@on.channel_post()
|
||||
async def save_message_id(
|
||||
self,
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> int:
|
||||
await state.update_data(last_message_id=message.message_id)
|
||||
return message.message_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize("update_type", ["channel_post", "edited_channel_post"])
|
||||
async def test_channel_events_with_scenes_do_not_require_fsm_state(
|
||||
bot: MockedBot,
|
||||
update_type: str,
|
||||
):
|
||||
dispatcher = Dispatcher()
|
||||
channel_router = Router()
|
||||
|
||||
if update_type == "channel_post":
|
||||
channel_router.channel_post.filter(F.chat.id == CHANNEL_ID)
|
||||
|
||||
@channel_router.channel_post()
|
||||
async def on_channel_post(message: Message):
|
||||
return message.message_id
|
||||
else:
|
||||
channel_router.edited_channel_post.filter(F.chat.id == CHANNEL_ID)
|
||||
|
||||
@channel_router.edited_channel_post()
|
||||
async def on_edited_channel_post(message: Message):
|
||||
return message.message_id
|
||||
|
||||
dispatcher.include_router(channel_router)
|
||||
SceneRegistry(dispatcher).add(BrowseScene)
|
||||
|
||||
message = Message(
|
||||
message_id=1,
|
||||
date=datetime.now(),
|
||||
chat=Chat(id=CHANNEL_ID, type="channel"),
|
||||
text="test",
|
||||
)
|
||||
|
||||
kwargs = {"update_id": 1, update_type: message}
|
||||
update = Update(**kwargs)
|
||||
|
||||
result = await dispatcher.feed_update(bot, update)
|
||||
assert result == 1
|
||||
|
||||
|
||||
async def test_channel_scene_has_fsm_state_with_chat_strategy(bot: MockedBot):
|
||||
dispatcher = Dispatcher(fsm_strategy=FSMStrategy.CHAT)
|
||||
router = Router()
|
||||
|
||||
@router.channel_post((F.chat.id == CHANNEL_ID) & (F.text == "enter"))
|
||||
async def enter_channel_state(message: Message, state: FSMContext):
|
||||
await state.set_state(ChannelStateScene.__scene_config__.state)
|
||||
return message.message_id
|
||||
|
||||
dispatcher.include_router(router)
|
||||
SceneRegistry(dispatcher).add(ChannelStateScene)
|
||||
|
||||
initial_update = Update(
|
||||
update_id=1,
|
||||
channel_post=Message(
|
||||
message_id=10,
|
||||
date=datetime.now(),
|
||||
chat=Chat(id=CHANNEL_ID, type="channel"),
|
||||
text="enter",
|
||||
),
|
||||
)
|
||||
await dispatcher.feed_update(bot, initial_update)
|
||||
|
||||
active_state = await dispatcher.fsm.storage.get_state(
|
||||
key=dispatcher.fsm.get_context(
|
||||
bot=bot,
|
||||
chat_id=CHANNEL_ID,
|
||||
user_id=CHANNEL_ID,
|
||||
).key
|
||||
)
|
||||
assert active_state == ChannelStateScene.__scene_config__.state
|
||||
|
||||
scene_update = Update(
|
||||
update_id=2,
|
||||
channel_post=Message(
|
||||
message_id=11,
|
||||
date=datetime.now(),
|
||||
chat=Chat(id=CHANNEL_ID, type="channel"),
|
||||
text="scene",
|
||||
),
|
||||
)
|
||||
result = await dispatcher.feed_update(bot, scene_update)
|
||||
assert result == 11
|
||||
state_data = await dispatcher.fsm.storage.get_data(
|
||||
key=dispatcher.fsm.get_context(
|
||||
bot=bot,
|
||||
chat_id=CHANNEL_ID,
|
||||
user_id=CHANNEL_ID,
|
||||
).key
|
||||
)
|
||||
assert state_data["last_message_id"] == 11
|
||||
Loading…
Add table
Add a link
Reference in a new issue