mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 15:24:06 +00:00
feat(slack): add groupRequireMention for allowlist channels
Slack's groupPolicy could either restrict to specific channels
("allowlist") or require an @mention ("mention"), but not both: in
allowlist mode the bot replied to every message in approved channels.
Add a groupRequireMention flag so that, when groupPolicy is "allowlist",
the bot only responds in channels listed in groupAllowFrom AND only when
@mentioned. Mirrors Signal's group.requireMention. No effect for the
"mention"/"open" policies, so existing configs are unchanged.
Extract the mention check into _is_mention and reuse it from both the
mention and allowlist branches.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ffae1dca6d
commit
2d9260cb9f
@ -572,7 +572,9 @@ nanobot gateway
|
|||||||
DM the bot directly or @mention it in a channel — it should respond!
|
DM the bot directly or @mention it in a channel — it should respond!
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> - `groupPolicy`: `"mention"` (default — respond only when @mentioned), `"open"` (respond to all channel messages), or `"allowlist"` (restrict to specific channels).
|
> - `groupPolicy`: `"mention"` (default — respond only when @mentioned), `"open"` (respond to all channel messages), or `"allowlist"` (restrict to specific channels via `groupAllowFrom`).
|
||||||
|
> - `groupAllowFrom`: channel IDs the bot may respond in when `groupPolicy` is `"allowlist"`.
|
||||||
|
> - `groupRequireMention`: when `true` and `groupPolicy` is `"allowlist"`, the bot only replies to channels in `groupAllowFrom` **and** only when @mentioned (instead of every message). No effect for `"mention"`/`"open"`. Use this to scope the bot to approved channels while keeping mention-only behavior.
|
||||||
> - DM policy defaults to open. Set `"dm": {"enabled": false}` to disable DMs.
|
> - DM policy defaults to open. Set `"dm": {"enabled": false}` to disable DMs.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@ -47,6 +47,10 @@ class SlackConfig(Base):
|
|||||||
allow_from: list[str] = Field(default_factory=list)
|
allow_from: list[str] = Field(default_factory=list)
|
||||||
group_policy: str = "mention"
|
group_policy: str = "mention"
|
||||||
group_allow_from: list[str] = Field(default_factory=list)
|
group_allow_from: list[str] = Field(default_factory=list)
|
||||||
|
# When group_policy is "allowlist", also require the bot to be @mentioned
|
||||||
|
# before responding (so it only replies to mentions in approved channels,
|
||||||
|
# instead of every message). No effect for "mention"/"open" policies.
|
||||||
|
group_require_mention: bool = False
|
||||||
dm: SlackDMConfig = Field(default_factory=SlackDMConfig)
|
dm: SlackDMConfig = Field(default_factory=SlackDMConfig)
|
||||||
|
|
||||||
|
|
||||||
@ -648,15 +652,22 @@ class SlackChannel(BaseChannel):
|
|||||||
return chat_id in self.config.group_allow_from
|
return chat_id in self.config.group_allow_from
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _is_mention(self, event_type: str, text: str) -> bool:
|
||||||
|
if event_type == "app_mention":
|
||||||
|
return True
|
||||||
|
return self._bot_user_id is not None and f"<@{self._bot_user_id}>" in text
|
||||||
|
|
||||||
def _should_respond_in_channel(self, event_type: str, text: str, chat_id: str) -> bool:
|
def _should_respond_in_channel(self, event_type: str, text: str, chat_id: str) -> bool:
|
||||||
if self.config.group_policy == "open":
|
if self.config.group_policy == "open":
|
||||||
return True
|
return True
|
||||||
if self.config.group_policy == "mention":
|
if self.config.group_policy == "mention":
|
||||||
if event_type == "app_mention":
|
return self._is_mention(event_type, text)
|
||||||
return True
|
|
||||||
return self._bot_user_id is not None and f"<@{self._bot_user_id}>" in text
|
|
||||||
if self.config.group_policy == "allowlist":
|
if self.config.group_policy == "allowlist":
|
||||||
return chat_id in self.config.group_allow_from
|
if chat_id not in self.config.group_allow_from:
|
||||||
|
return False
|
||||||
|
if self.config.group_require_mention:
|
||||||
|
return self._is_mention(event_type, text)
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_allowed(self, sender_id: str) -> bool:
|
def is_allowed(self, sender_id: str) -> bool:
|
||||||
|
|||||||
@ -655,3 +655,62 @@ def test_slack_channel_uses_channel_aware_allow_policy() -> None:
|
|||||||
channel = SlackChannel(SlackConfig(enabled=True, allow_from=[]), MessageBus())
|
channel = SlackChannel(SlackConfig(enabled=True, allow_from=[]), MessageBus())
|
||||||
assert channel.is_allowed("U1") is True
|
assert channel.is_allowed("U1") is True
|
||||||
assert channel._is_allowed("U1", "C123", "channel") is True
|
assert channel._is_allowed("U1", "C123", "channel") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_mention_policy_responds_to_mentions_in_any_channel() -> None:
|
||||||
|
channel = SlackChannel(SlackConfig(enabled=True, group_policy="mention"), MessageBus())
|
||||||
|
channel._bot_user_id = "UBOT"
|
||||||
|
|
||||||
|
assert channel._should_respond_in_channel("app_mention", "<@UBOT> hi", "C123") is True
|
||||||
|
assert channel._should_respond_in_channel("message", "<@UBOT> hi", "C999") is True
|
||||||
|
assert channel._should_respond_in_channel("message", "no mention here", "C123") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_allowlist_policy_restricts_to_approved_channels() -> None:
|
||||||
|
channel = SlackChannel(
|
||||||
|
SlackConfig(enabled=True, group_policy="allowlist", group_allow_from=["C_OK"]),
|
||||||
|
MessageBus(),
|
||||||
|
)
|
||||||
|
channel._bot_user_id = "UBOT"
|
||||||
|
|
||||||
|
# In an approved channel without require_mention, respond to anything.
|
||||||
|
assert channel._should_respond_in_channel("message", "anything", "C_OK") is True
|
||||||
|
# An unapproved channel is always rejected.
|
||||||
|
assert channel._should_respond_in_channel("app_mention", "<@UBOT> hi", "C_NOPE") is False
|
||||||
|
# _is_allowed also gates on the channel allowlist.
|
||||||
|
assert channel._is_allowed("U1", "C_OK", "channel") is True
|
||||||
|
assert channel._is_allowed("U1", "C_NOPE", "channel") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_allowlist_with_require_mention_needs_both_channel_and_mention() -> None:
|
||||||
|
channel = SlackChannel(
|
||||||
|
SlackConfig(
|
||||||
|
enabled=True,
|
||||||
|
group_policy="allowlist",
|
||||||
|
group_allow_from=["C_OK"],
|
||||||
|
group_require_mention=True,
|
||||||
|
),
|
||||||
|
MessageBus(),
|
||||||
|
)
|
||||||
|
channel._bot_user_id = "UBOT"
|
||||||
|
|
||||||
|
# Approved channel + mention -> respond.
|
||||||
|
assert channel._should_respond_in_channel("app_mention", "<@UBOT> hi", "C_OK") is True
|
||||||
|
assert channel._should_respond_in_channel("message", "<@UBOT> hi", "C_OK") is True
|
||||||
|
# Approved channel but no mention -> stay quiet.
|
||||||
|
assert channel._should_respond_in_channel("message", "just chatting", "C_OK") is False
|
||||||
|
# Mention in an unapproved channel -> stay quiet.
|
||||||
|
assert channel._should_respond_in_channel("app_mention", "<@UBOT> hi", "C_NOPE") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_require_mention_accepts_camel_case_alias() -> None:
|
||||||
|
config = SlackConfig.model_validate(
|
||||||
|
{
|
||||||
|
"enabled": True,
|
||||||
|
"groupPolicy": "allowlist",
|
||||||
|
"groupAllowFrom": ["C_OK"],
|
||||||
|
"groupRequireMention": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert config.group_require_mention is True
|
||||||
|
assert config.group_allow_from == ["C_OK"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user