Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion gateway/platforms/feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ class FeishuAdapterSettings:
ws_reconnect_interval: int = 120
ws_ping_interval: Optional[int] = None
ws_ping_timeout: Optional[int] = None
require_mention: bool = True
admins: frozenset[str] = frozenset()
default_group_policy: str = ""
group_rules: Dict[str, FeishuGroupRule] = field(default_factory=dict)
Expand Down Expand Up @@ -309,6 +310,21 @@ def _to_boolean(value: Any) -> bool:
return value is True or value == 1 or value == "true"


def _feishu_require_mention(extra: Dict[str, Any]) -> bool:
"""Parse FEISHU_REQUIRE_MENTION using explicit-false semantics.

The safe default is True (mention gating on). Only ``false``, ``0``,
``no``, or ``off`` disable it — matching the pattern used by Slack,
Discord, and Matrix adapters.
"""
configured = extra.get("require_mention")
if configured is not None:
if isinstance(configured, str):
return configured.strip().lower() not in ("false", "0", "no", "off")
return bool(configured)
return os.getenv("FEISHU_REQUIRE_MENTION", "true").strip().lower() not in ("false", "0", "no", "off")


def _is_style_enabled(style: Dict[str, Any] | None, key: str) -> bool:
if not style:
return False
Expand Down Expand Up @@ -1140,6 +1156,7 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings:
ws_ping_interval=_coerce_int(extra.get("ws_ping_interval"), default=None, min_value=1),
ws_ping_timeout=_coerce_int(extra.get("ws_ping_timeout"), default=None, min_value=1),
admins=admins,
require_mention=_feishu_require_mention(extra),
default_group_policy=default_group_policy,
group_rules=group_rules,
)
Expand All @@ -1154,6 +1171,7 @@ def _apply_settings(self, settings: FeishuAdapterSettings) -> None:
self._group_policy = settings.group_policy
self._allowed_group_users = set(settings.allowed_group_users)
self._admins = set(settings.admins)
self._require_mention = settings.require_mention
self._default_group_policy = settings.default_group_policy or settings.group_policy
self._group_rules = settings.group_rules
self._bot_open_id = settings.bot_open_id
Expand Down Expand Up @@ -3021,9 +3039,16 @@ def _allow_group_message(self, sender_id: Any, chat_id: str = "") -> bool:
return bool(sender_ids and (sender_ids & self._allowed_group_users))

def _should_accept_group_message(self, message: Any, sender_id: Any, chat_id: str = "") -> bool:
"""Require an explicit @mention before group messages enter the agent."""
"""Gate group messages: policy check first, then optional mention check.

When ``require_mention`` is False (controlled via ``FEISHU_REQUIRE_MENTION``
env var or config ``extra[\"require_mention\"]``), the mention gate is
skipped entirely after the policy gate passes.
"""
if not self._allow_group_message(sender_id, chat_id):
return False
if not self._require_mention:
return True
# @_all is Feishu's @everyone placeholder — always route to the bot.
raw_content = getattr(message, "content", "") or ""
if "@_all" in raw_content:
Expand Down
175 changes: 175 additions & 0 deletions tests/gateway/test_feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2922,3 +2922,178 @@ async def _direct(func, *args, **kwargs):
result = asyncio.run(adapter._resolve_sender_name_from_api("ou_broken"))

self.assertIsNone(result)


class TestRequireMention(unittest.TestCase):
"""Tests for the FEISHU_REQUIRE_MENTION configuration (closes #5465)."""

# ── Parsing: _feishu_require_mention helper ──────────────────────────

def test_parse_require_mention_defaults_to_true(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {}, clear=True):
self.assertTrue(_feishu_require_mention({}))

def test_parse_require_mention_explicit_true(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": "true"}, clear=True):
self.assertTrue(_feishu_require_mention({}))

def test_parse_require_mention_false(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": "false"}, clear=True):
self.assertFalse(_feishu_require_mention({}))

def test_parse_require_mention_zero(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": "0"}, clear=True):
self.assertFalse(_feishu_require_mention({}))

def test_parse_require_mention_no(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": "no"}, clear=True):
self.assertFalse(_feishu_require_mention({}))

def test_parse_require_mention_off(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": "off"}, clear=True):
self.assertFalse(_feishu_require_mention({}))

def test_parse_require_mention_random_string_stays_true(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": "yes"}, clear=True):
self.assertTrue(_feishu_require_mention({}))

def test_parse_require_mention_empty_string_stays_true(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": ""}, clear=True):
self.assertTrue(_feishu_require_mention({}))

def test_parse_require_mention_config_extra_overrides_env(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": "true"}, clear=True):
self.assertFalse(_feishu_require_mention({"require_mention": "false"}))

def test_parse_require_mention_config_extra_bool_true(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {}, clear=True):
self.assertTrue(_feishu_require_mention({"require_mention": True}))

def test_parse_require_mention_config_extra_bool_false(self):
from gateway.platforms.feishu import _feishu_require_mention

with patch.dict(os.environ, {"FEISHU_REQUIRE_MENTION": "true"}, clear=True):
self.assertFalse(_feishu_require_mention({"require_mention": False}))

# ── Adapter integration ──────────────────────────────────────────────

@patch.dict(
os.environ,
{"FEISHU_GROUP_POLICY": "open", "FEISHU_REQUIRE_MENTION": "false"},
clear=True,
)
def test_group_message_accepted_without_mention_when_require_mention_false(self):
"""When require_mention=false, group messages bypass the mention gate."""
from gateway.config import PlatformConfig
from gateway.platforms.feishu import FeishuAdapter

adapter = FeishuAdapter(PlatformConfig())
message = SimpleNamespace(mentions=[], content='{"text":"hello"}', message_type="text")
sender_id = SimpleNamespace(open_id="ou_any", user_id=None)

self.assertTrue(adapter._should_accept_group_message(message, sender_id, ""))

@patch.dict(
os.environ,
{"FEISHU_GROUP_POLICY": "open", "FEISHU_REQUIRE_MENTION": "false"},
clear=True,
)
def test_group_message_still_requires_policy_gate_when_require_mention_false(self):
"""Policy gate still applies even when mention gate is disabled."""
from gateway.config import PlatformConfig
from gateway.platforms.feishu import FeishuAdapter

config = PlatformConfig(
extra={
"group_rules": {
"oc_chat_a": {"policy": "disabled"},
},
"require_mention": False,
}
)
adapter = FeishuAdapter(config)
message = SimpleNamespace(mentions=[], content='{"text":"hello"}', message_type="text")
sender_id = SimpleNamespace(open_id="ou_any", user_id=None)

self.assertFalse(adapter._should_accept_group_message(message, sender_id, "oc_chat_a"))

@patch.dict(
os.environ,
{"FEISHU_GROUP_POLICY": "allowlist", "FEISHU_ALLOWED_USERS": "ou_allowed", "FEISHU_REQUIRE_MENTION": "false"},
clear=True,
)
def test_allowlist_policy_still_applies_without_mention(self):
"""With require_mention=false + allowlist policy, only allowed users pass."""
from gateway.config import PlatformConfig
from gateway.platforms.feishu import FeishuAdapter

adapter = FeishuAdapter(PlatformConfig())
message = SimpleNamespace(mentions=[], content='{"text":"hello"}', message_type="text")

self.assertTrue(
adapter._should_accept_group_message(
message,
SimpleNamespace(open_id="ou_allowed", user_id=None),
"",
)
)
self.assertFalse(
adapter._should_accept_group_message(
message,
SimpleNamespace(open_id="ou_blocked", user_id=None),
"",
)
)

@patch.dict(
os.environ,
{"FEISHU_GROUP_POLICY": "open"},
clear=True,
)
def test_default_require_mention_still_enforces_mention(self):
"""Backward compat: when FEISHU_REQUIRE_MENTION is not set, mention is required."""
from gateway.config import PlatformConfig
from gateway.platforms.feishu import FeishuAdapter

adapter = FeishuAdapter(PlatformConfig())
message = SimpleNamespace(mentions=[], content='{"text":"hello"}', message_type="text")
sender_id = SimpleNamespace(open_id="ou_any", user_id=None)

self.assertFalse(adapter._should_accept_group_message(message, sender_id, ""))

@patch.dict(
os.environ,
{"FEISHU_GROUP_POLICY": "open", "FEISHU_REQUIRE_MENTION": "false"},
clear=True,
)
def test_require_mention_false_via_config_extra(self):
"""require_mention=false can also be set via PlatformConfig.extra."""
from gateway.config import PlatformConfig
from gateway.platforms.feishu import FeishuAdapter

config = PlatformConfig(extra={"require_mention": "false"})
adapter = FeishuAdapter(config)
message = SimpleNamespace(mentions=[], content='{"text":"hello"}', message_type="text")
sender_id = SimpleNamespace(open_id="ou_any", user_id=None)

self.assertTrue(adapter._should_accept_group_message(message, sender_id, ""))