fix(slack): honor documented mention_patterns wake words
The Slack docs document `slack.mention_patterns` as custom wake words that trigger the bot alongside `@mention`, and the config layer bridges the key into the Slack adapter's `config.extra` — but the adapter never read it. With `require_mention` on, a channel message containing a configured wake word (and no literal `<@BOTUID>`) was silently ignored. Every other adapter that documents `mention_patterns` (Telegram, DingTalk, Mattermost, WhatsApp, BlueBubbles, Photon) implements it; Slack was the odd one out. Add `_slack_mention_patterns()` (compiled, cached; reads `slack.mention_patterns` as a list/string or `SLACK_MENTION_PATTERNS` as a JSON/CSV/newline list, invalid regexes warned and skipped) and `_slack_message_matches_mention_patterns()`, mirroring the existing adapters. Channel mention detection now also triggers on a wake-word match, so the documented field works as described. Adds tests for pattern compilation (list/string/env/invalid-regex) and for the channel-trigger gating with a wake word under require_mention.
This commit is contained in:
parent
2617946397
commit
4966268764
2 changed files with 116 additions and 3 deletions
|
|
@ -2485,7 +2485,10 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
# 4. There's an existing session for this thread (survives restarts)
|
||||
bot_uid = self._team_bot_user_ids.get(team_id, self._bot_user_id)
|
||||
routing_text = original_text or ""
|
||||
is_mentioned = bot_uid and f"<@{bot_uid}>" in routing_text
|
||||
is_mentioned = bool(
|
||||
(bot_uid and f"<@{bot_uid}>" in routing_text)
|
||||
or self._slack_message_matches_mention_patterns(routing_text)
|
||||
)
|
||||
event_thread_ts = event.get("thread_ts")
|
||||
is_thread_reply = bool(event_thread_ts and event_thread_ts != ts)
|
||||
|
||||
|
|
@ -3812,6 +3815,62 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
||||
def _slack_mention_patterns(self) -> List["re.Pattern"]:
|
||||
"""Compile optional regex wake-word patterns for channel triggers.
|
||||
|
||||
Parity with the other adapters (Telegram, DingTalk, Mattermost,
|
||||
WhatsApp, BlueBubbles, Photon): when ``require_mention`` is on, a
|
||||
channel message matching one of these patterns triggers the bot even
|
||||
without a literal ``<@BOTUID>`` mention. Reads ``slack.mention_patterns``
|
||||
(a list or single string) or ``SLACK_MENTION_PATTERNS`` (a JSON list, or
|
||||
newline/comma-separated values). Compiled patterns are cached on the
|
||||
instance. Previously this documented field was silently dropped.
|
||||
"""
|
||||
cached = getattr(self, "_compiled_mention_patterns", None)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
patterns = self.config.extra.get("mention_patterns") if self.config.extra else None
|
||||
if patterns is None:
|
||||
raw = os.getenv("SLACK_MENTION_PATTERNS", "").strip()
|
||||
if raw:
|
||||
try:
|
||||
import json as _json
|
||||
patterns = _json.loads(raw)
|
||||
except Exception:
|
||||
patterns = [p.strip() for p in raw.splitlines() if p.strip()] or [
|
||||
p.strip() for p in raw.split(",") if p.strip()
|
||||
]
|
||||
|
||||
if isinstance(patterns, str):
|
||||
patterns = [patterns]
|
||||
|
||||
compiled: List["re.Pattern"] = []
|
||||
if isinstance(patterns, list):
|
||||
for pat in patterns:
|
||||
if not isinstance(pat, str) or not pat.strip():
|
||||
continue
|
||||
try:
|
||||
compiled.append(re.compile(pat, re.IGNORECASE))
|
||||
except re.error as exc:
|
||||
logger.warning("[Slack] Invalid mention pattern %r: %s", pat, exc)
|
||||
elif patterns is not None:
|
||||
logger.warning(
|
||||
"[Slack] mention_patterns must be a list or string; got %s",
|
||||
type(patterns).__name__,
|
||||
)
|
||||
|
||||
if compiled:
|
||||
logger.info("[Slack] Loaded %d mention pattern(s)", len(compiled))
|
||||
self._compiled_mention_patterns = compiled
|
||||
return compiled
|
||||
|
||||
def _slack_message_matches_mention_patterns(self, text: str) -> bool:
|
||||
"""Return True when ``text`` matches a configured wake-word pattern."""
|
||||
if not text:
|
||||
return False
|
||||
return any(pattern.search(text) for pattern in self._slack_mention_patterns())
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Plugin migration glue (#41112 / #3823)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ CHANNEL_ID = "C0AQWDLHY9M"
|
|||
OTHER_CHANNEL_ID = "C9999999999"
|
||||
|
||||
|
||||
def _make_adapter(require_mention=None, strict_mention=None, free_response_channels=None, allowed_channels=None):
|
||||
def _make_adapter(require_mention=None, strict_mention=None, free_response_channels=None,
|
||||
allowed_channels=None, mention_patterns=None):
|
||||
extra = {}
|
||||
if require_mention is not None:
|
||||
extra["require_mention"] = require_mention
|
||||
|
|
@ -65,6 +66,8 @@ def _make_adapter(require_mention=None, strict_mention=None, free_response_chann
|
|||
extra["free_response_channels"] = free_response_channels
|
||||
if allowed_channels is not None:
|
||||
extra["allowed_channels"] = allowed_channels
|
||||
if mention_patterns is not None:
|
||||
extra["mention_patterns"] = mention_patterns
|
||||
|
||||
adapter = object.__new__(SlackAdapter)
|
||||
adapter.platform = Platform.SLACK
|
||||
|
|
@ -249,7 +252,10 @@ def _would_process(adapter, *, is_dm=False, channel_id=CHANNEL_ID,
|
|||
bot_uid = adapter._team_bot_user_ids.get("T1", adapter._bot_user_id)
|
||||
if mentioned:
|
||||
text = f"<@{bot_uid}> {text}"
|
||||
is_mentioned = bot_uid and f"<@{bot_uid}>" in text
|
||||
is_mentioned = bool(
|
||||
(bot_uid and f"<@{bot_uid}>" in text)
|
||||
or adapter._slack_message_matches_mention_patterns(text)
|
||||
)
|
||||
|
||||
if not is_dm and bot_uid:
|
||||
# allowed_channels check (whitelist — must pass before other gating)
|
||||
|
|
@ -687,3 +693,51 @@ def test_config_bridges_slack_allowed_channels_env_takes_precedence(monkeypatch,
|
|||
import os as _os
|
||||
# env var must not be overwritten by config.yaml
|
||||
assert _os.environ["SLACK_ALLOWED_CHANNELS"] == OTHER_CHANNEL_ID
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: mention_patterns (wake words) — parity with other adapters (#50732)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_mention_patterns_default_no_match(monkeypatch):
|
||||
monkeypatch.delenv("SLACK_MENTION_PATTERNS", raising=False)
|
||||
adapter = _make_adapter()
|
||||
assert adapter._slack_mention_patterns() == []
|
||||
assert adapter._slack_message_matches_mention_patterns("hello there") is False
|
||||
|
||||
|
||||
def test_mention_patterns_list_matches():
|
||||
adapter = _make_adapter(mention_patterns=["hey hermes", "hermes,"])
|
||||
assert adapter._slack_message_matches_mention_patterns("hey hermes, you there?") is True
|
||||
assert adapter._slack_message_matches_mention_patterns("just chatting") is False
|
||||
|
||||
|
||||
def test_mention_patterns_case_insensitive():
|
||||
adapter = _make_adapter(mention_patterns=["hey hermes"])
|
||||
assert adapter._slack_message_matches_mention_patterns("HEY HERMES!") is True
|
||||
|
||||
|
||||
def test_mention_patterns_single_string():
|
||||
adapter = _make_adapter(mention_patterns="^hermes")
|
||||
assert adapter._slack_message_matches_mention_patterns("hermes do this") is True
|
||||
assert adapter._slack_message_matches_mention_patterns("ok hermes") is False
|
||||
|
||||
|
||||
def test_mention_patterns_invalid_regex_skipped_without_crash():
|
||||
# An invalid pattern is dropped; valid siblings still work.
|
||||
adapter = _make_adapter(mention_patterns=["(unclosed", "hey hermes"])
|
||||
assert adapter._slack_message_matches_mention_patterns("hey hermes") is True
|
||||
|
||||
|
||||
def test_mention_patterns_env_var_fallback(monkeypatch):
|
||||
monkeypatch.setenv("SLACK_MENTION_PATTERNS", '["hey hermes", "hermes,"]')
|
||||
adapter = _make_adapter() # no config value -> falls back to env
|
||||
assert adapter._slack_message_matches_mention_patterns("hey hermes") is True
|
||||
|
||||
|
||||
def test_mention_patterns_trigger_in_channel_without_literal_mention():
|
||||
"""A wake word triggers the bot in a channel even with require_mention on."""
|
||||
adapter = _make_adapter(require_mention=True, mention_patterns=["hey hermes"])
|
||||
assert _would_process(adapter, text="hey hermes what's the status") is True
|
||||
# Unrelated channel chatter is still ignored.
|
||||
assert _would_process(adapter, text="lunch anyone?") is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue