From 58400a67931f3aea31b6dd2631caf36ea6a4fbcf Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:36:26 -0700 Subject: [PATCH] refactor(gateway): promote compile_mention_patterns to helpers --- gateway/platforms/bluebubbles.py | 36 ++------- gateway/platforms/helpers.py | 102 ++++++++++++++++++++++++++ plugins/platforms/dingtalk/adapter.py | 32 ++------ plugins/platforms/photon/adapter.py | 36 ++------- plugins/platforms/telegram/adapter.py | 31 ++------ 5 files changed, 132 insertions(+), 105 deletions(-) diff --git a/gateway/platforms/bluebubbles.py b/gateway/platforms/bluebubbles.py index c88240f01c7..1cb8c040cd9 100644 --- a/gateway/platforms/bluebubbles.py +++ b/gateway/platforms/bluebubbles.py @@ -32,7 +32,7 @@ from gateway.platforms.base import ( cache_document_from_bytes, ) from .media_cache import ext_for_mime -from gateway.platforms.helpers import strip_markdown +from gateway.platforms.helpers import compile_mention_patterns, strip_markdown # Historical BlueBubbles mime→ext maps, preserved verbatim as overrides for # the shared dispatch in gateway.platforms.media_cache. Both maps are @@ -194,34 +194,12 @@ class BlueBubblesAdapter(BasePlatformAdapter): ``raw`` is a list (from config or env JSON), a string (raw env var: JSON list, or comma/newline-separated), or None (use Hermes defaults). """ - if raw is None: - patterns = list(DEFAULT_MENTION_PATTERNS) - elif isinstance(raw, str): - text = raw.strip() - try: - loaded = json.loads(text) if text else [] - except Exception: - loaded = None - patterns = loaded if isinstance(loaded, list) else [ - part.strip() - for line in text.splitlines() - for part in line.split(",") - ] - elif isinstance(raw, list): - patterns = raw - else: - patterns = [raw] - - compiled: List["re.Pattern"] = [] - for pattern in patterns: - text = str(pattern).strip() - if not text: - continue - try: - compiled.append(re.compile(text, re.IGNORECASE)) - except re.error as exc: - logger.warning("[bluebubbles] Invalid mention pattern %r: %s", text, exc) - return compiled + return compile_mention_patterns( + raw, + log_prefix="bluebubbles", + defaults=DEFAULT_MENTION_PATTERNS, + logger_=logger, + ) def _message_matches_mention_patterns(self, text: str) -> bool: if not text or not self._mention_patterns: diff --git a/gateway/platforms/helpers.py b/gateway/platforms/helpers.py index 85edb02c514..c99020abc89 100644 --- a/gateway/platforms/helpers.py +++ b/gateway/platforms/helpers.py @@ -417,3 +417,105 @@ def convert_table_to_bullets(text: str) -> str: i += 1 return '\n'.join(out) + + +# ─── Mention-pattern compilation ───────────────────────────────────────────── + + +def compile_mention_patterns( + raw, + *, + log_prefix: str, + platform_label: str | None = None, + display_label: str | None = None, + defaults: 'list[str] | None' = None, + logger_: 'logging.Logger | None' = None, +) -> 'list[re.Pattern]': + """Compile regex wake-word/mention patterns from config or env values. + + Two adapter families share this logic: + + * **Config-style** (dingtalk, telegram): pass ``platform_label`` (e.g. + ``"dingtalk"``). ``raw`` is the value from ``config.extra`` after env + fallback parsing; must be a list or string, anything else logs a warning + and yields ``[]``. Non-string entries are skipped. A summary info log is + emitted when patterns load. + * **Wakeword-style** (photon, bluebubbles): pass ``defaults``. ``raw`` may + be None (use defaults), a string (JSON list or comma/newline separated), + a list, or a scalar (wrapped in a list). Entries are coerced via + ``str()``. + + ``log_prefix`` is interpolated into every log message so per-adapter log + output stays byte-identical to the historical inline implementations. + """ + log = logger_ or logger + + if platform_label is not None: + # Config-style (dingtalk/telegram) semantics. + display = display_label or platform_label + patterns = raw + if patterns is None: + return [] + if isinstance(patterns, str): + patterns = [patterns] + if not isinstance(patterns, list): + log.warning( + "[%s] %s mention_patterns must be a list or string; got %s", + log_prefix, + platform_label, + type(patterns).__name__, + ) + return [] + + compiled: list[re.Pattern] = [] + for pattern in patterns: + if not isinstance(pattern, str) or not pattern.strip(): + continue + try: + compiled.append(re.compile(pattern, re.IGNORECASE)) + except re.error as exc: + log.warning( + "[%s] Invalid %s mention pattern %r: %s", + log_prefix, + display, + pattern, + exc, + ) + if compiled: + log.info( + "[%s] Loaded %d %s mention pattern(s)", + log_prefix, + len(compiled), + display, + ) + return compiled + + # Wakeword-style (photon/bluebubbles) semantics. + if raw is None: + patterns = list(defaults or []) + elif isinstance(raw, str): + text = raw.strip() + try: + loaded = json.loads(text) if text else [] + except Exception: + loaded = None + patterns = loaded if isinstance(loaded, list) else [ + part.strip() + for line in text.splitlines() + for part in line.split(",") + ] + elif isinstance(raw, list): + patterns = raw + else: + patterns = [raw] + + compiled = [] + for pattern in patterns: + text = str(pattern).strip() + if not text: + continue + try: + compiled.append(re.compile(text, re.IGNORECASE)) + except re.error as exc: + log.warning("[%s] Invalid mention pattern %r: %s", log_prefix, text, exc) + return compiled diff --git a/plugins/platforms/dingtalk/adapter.py b/plugins/platforms/dingtalk/adapter.py index 0ad470d1c4c..ccfdd0b7393 100644 --- a/plugins/platforms/dingtalk/adapter.py +++ b/plugins/platforms/dingtalk/adapter.py @@ -95,7 +95,7 @@ except Exception: tea_util_models = None from gateway.config import Platform, PlatformConfig -from gateway.platforms.helpers import MessageDeduplicator +from gateway.platforms.helpers import MessageDeduplicator, compile_mention_patterns from gateway.platforms.base import ( BasePlatformAdapter, MessageEvent, @@ -458,29 +458,13 @@ class DingTalkAdapter(BasePlatformAdapter): loaded = [part.strip() for part in raw.split(",") if part.strip()] patterns = loaded - if patterns is None: - return [] - if isinstance(patterns, str): - patterns = [patterns] - if not isinstance(patterns, list): - logger.warning( - "[%s] dingtalk mention_patterns must be a list or string; got %s", - self.name, - type(patterns).__name__, - ) - return [] - - compiled: List[re.Pattern] = [] - for pattern in patterns: - if not isinstance(pattern, str) or not pattern.strip(): - continue - try: - compiled.append(re.compile(pattern, re.IGNORECASE)) - except re.error as exc: - logger.warning("[%s] Invalid DingTalk mention pattern %r: %s", self.name, pattern, exc) - if compiled: - logger.info("[%s] Loaded %d DingTalk mention pattern(s)", self.name, len(compiled)) - return compiled + return compile_mention_patterns( + patterns, + log_prefix=self.name, + platform_label="dingtalk", + display_label="DingTalk", + logger_=logger, + ) def _load_allowed_users(self) -> Set[str]: """Load allowed-users list from config.extra or env var. diff --git a/plugins/platforms/photon/adapter.py b/plugins/platforms/photon/adapter.py index 6b9fa246131..79e10c2d593 100644 --- a/plugins/platforms/photon/adapter.py +++ b/plugins/platforms/photon/adapter.py @@ -63,7 +63,7 @@ from gateway.platforms.base import ( ProcessingOutcome, SendResult, ) -from gateway.platforms.helpers import strip_markdown +from gateway.platforms.helpers import compile_mention_patterns, strip_markdown from .auth import load_project_credentials @@ -823,34 +823,12 @@ class PhotonAdapter(BasePlatformAdapter): Mirrors the BlueBubbles implementation so both iMessage channels accept the same configuration shapes. """ - if raw is None: - patterns = list(_DEFAULT_MENTION_PATTERNS) - elif isinstance(raw, str): - text = raw.strip() - try: - loaded = json.loads(text) if text else [] - except Exception: - loaded = None - patterns = loaded if isinstance(loaded, list) else [ - part.strip() - for line in text.splitlines() - for part in line.split(",") - ] - elif isinstance(raw, list): - patterns = raw - else: - patterns = [raw] - - compiled: "list[re.Pattern]" = [] - for pattern in patterns: - text = str(pattern).strip() - if not text: - continue - try: - compiled.append(re.compile(text, re.IGNORECASE)) - except re.error as exc: - logger.warning("[photon] Invalid mention pattern %r: %s", text, exc) - return compiled + return compile_mention_patterns( + raw, + log_prefix="photon", + defaults=_DEFAULT_MENTION_PATTERNS, + logger_=logger, + ) def _message_matches_mention_patterns(self, text: str) -> bool: if not text or not self._mention_patterns: diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index cd0fdde6fb0..3f8538d8832 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -490,6 +490,7 @@ def _separate_chunk_indicator_from_fence(text: str) -> str: from gateway.platforms.helpers import ( TABLE_SEPARATOR_RE as _TABLE_SEPARATOR_RE, + compile_mention_patterns, convert_table_to_bullets as _wrap_markdown_tables, ) @@ -7821,29 +7822,13 @@ class TelegramAdapter(BasePlatformAdapter): loaded = [part.strip() for part in raw.split(",") if part.strip()] patterns = loaded - if patterns is None: - return [] - if isinstance(patterns, str): - patterns = [patterns] - if not isinstance(patterns, list): - logger.warning( - "[%s] telegram mention_patterns must be a list or string; got %s", - self.name, - type(patterns).__name__, - ) - return [] - - compiled: List[re.Pattern] = [] - for pattern in patterns: - if not isinstance(pattern, str) or not pattern.strip(): - continue - try: - compiled.append(re.compile(pattern, re.IGNORECASE)) - except re.error as exc: - logger.warning("[%s] Invalid Telegram mention pattern %r: %s", self.name, pattern, exc) - if compiled: - logger.info("[%s] Loaded %d Telegram mention pattern(s)", self.name, len(compiled)) - return compiled + return compile_mention_patterns( + patterns, + log_prefix=self.name, + platform_label="telegram", + display_label="Telegram", + logger_=logger, + ) def _is_group_chat(self, message: Message) -> bool: chat = getattr(message, "chat", None)