diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index 718f01e9954d..f659f6197843 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -2390,8 +2390,7 @@ async def _handle_reaction_event(self, event_type: str, data: Any) -> None: if not msg: return sender = getattr(msg, "sender", None) - sender_type = str(getattr(sender, "sender_type", "") or "").lower() - if sender_type != "app": + if not self._is_own_bot_sender(sender): return # only route reactions on our own bot messages chat_id = str(getattr(msg, "chat_id", "") or "") chat_type_raw = str(getattr(msg, "chat_type", "p2p") or "p2p") @@ -3647,6 +3646,10 @@ def _should_accept_group_message(self, message: Any, sender_id: Any, chat_id: st def _is_self_sent_bot_message(self, event: Any) -> bool: """Return True only for Feishu events emitted by this Hermes bot.""" sender = getattr(event, "sender", None) + return self._is_own_bot_sender(sender) + + def _is_own_bot_sender(self, sender: Any) -> bool: + """Return True only for senders that match this Hermes bot identity.""" sender_type = str(getattr(sender, "sender_type", "") or "").strip().lower() if sender_type not in {"bot", "app"}: return False diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index f21b7dcef829..8f978ea0d265 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -689,6 +689,104 @@ def _close_coro_and_return_future(coro, _loop): adapter._on_reaction_event("im.message.reaction.created_v1", data) run_threadsafe.assert_called_once() + @patch.dict( + os.environ, + { + "FEISHU_BOT_OPEN_ID": "ou_hermes", + "FEISHU_BOT_USER_ID": "u_hermes", + }, + clear=True, + ) + def test_user_reaction_on_other_app_message_is_dropped(self): + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + adapter._client = Mock() + adapter._client.im.v1.message.get = Mock() + adapter._build_get_message_request = Mock(return_value=object()) + adapter._handle_message_with_guards = AsyncMock() + + target_message = SimpleNamespace( + sender=SimpleNamespace( + sender_type="app", + sender_id=SimpleNamespace(open_id="ou_other_bot", user_id="u_other_bot"), + ), + chat_id="oc_group", + chat_type="group", + ) + response = Mock() + response.success = Mock(return_value=True) + response.data = SimpleNamespace(items=[target_message]) + adapter._client.im.v1.message.get.return_value = response + + data = SimpleNamespace( + event=SimpleNamespace( + message_id="om_other_bot", + user_id=SimpleNamespace(open_id="ou_user", user_id="u_user", union_id="on_user"), + reaction_type=SimpleNamespace(emoji_type="THUMBSUP"), + ) + ) + + asyncio.run(adapter._handle_reaction_event("im.message.reaction.created_v1", data)) + + adapter._handle_message_with_guards.assert_not_awaited() + + @patch.dict( + os.environ, + { + "FEISHU_BOT_OPEN_ID": "ou_hermes", + "FEISHU_BOT_USER_ID": "u_hermes", + }, + clear=True, + ) + def test_user_reaction_on_own_app_message_is_routed(self): + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + adapter._client = Mock() + adapter._client.im.v1.message.get = Mock() + adapter._build_get_message_request = Mock(return_value=object()) + adapter._handle_message_with_guards = AsyncMock() + adapter._resolve_sender_profile = AsyncMock( + return_value={ + "user_id": "u_user", + "user_name": "Feishu User", + "user_id_alt": "on_user", + } + ) + adapter.get_chat_info = AsyncMock(return_value={"name": "Feishu Group", "type": "group"}) + + target_message = SimpleNamespace( + sender=SimpleNamespace( + sender_type="app", + sender_id=SimpleNamespace(open_id="ou_hermes", user_id="u_hermes"), + ), + chat_id="oc_group", + chat_type="group", + ) + response = Mock() + response.success = Mock(return_value=True) + response.data = SimpleNamespace(items=[target_message]) + adapter._client.im.v1.message.get.return_value = response + + data = SimpleNamespace( + event=SimpleNamespace( + message_id="om_hermes", + user_id=SimpleNamespace(open_id="ou_user", user_id="u_user", union_id="on_user"), + reaction_type=SimpleNamespace(emoji_type="THUMBSUP"), + ) + ) + + asyncio.run(adapter._handle_reaction_event("im.message.reaction.created_v1", data)) + + adapter._handle_message_with_guards.assert_awaited_once() + event = adapter._handle_message_with_guards.await_args.args[0] + self.assertEqual(event.text, "reaction:added:THUMBSUP") + self.assertEqual(event.source.chat_id, "oc_group") + self.assertEqual(event.source.user_id, "u_user") + @patch.dict(os.environ, {"FEISHU_GROUP_POLICY": "open"}, clear=True) def test_group_message_requires_mentions_even_when_policy_open(self): from gateway.config import PlatformConfig