From 0326ba1bcff46150279a6dc4724f3faa67aad71f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:52:49 -0700 Subject: [PATCH] refactor(matrix): unify mention localpart/state-event lookups, room identity build, picker choice tables; drop _build_adapter wrapper; pack constants (3327->3210) --- plugins/platforms/matrix/adapter.py | 315 +++++++++------------------- 1 file changed, 99 insertions(+), 216 deletions(-) diff --git a/plugins/platforms/matrix/adapter.py b/plugins/platforms/matrix/adapter.py index 1ac976a810..f4ae705a03 100644 --- a/plugins/platforms/matrix/adapter.py +++ b/plugins/platforms/matrix/adapter.py @@ -40,8 +40,7 @@ from agent.secret_scope import UnscopedSecretError, get_secret try: from mautrix.types import ( - ContentURI, EventID, EventType, PresenceState, RoomCreatePreset, RoomID, TrustState, UserID, - ) + ContentURI, EventID, EventType, PresenceState, RoomCreatePreset, RoomID, TrustState, UserID) except ImportError: # Import-safe stubs without mautrix: check_matrix_requirements() gates production use, but # tests exercise adapter methods so the attributes must exist. @@ -49,11 +48,9 @@ except ImportError: EventType = type("_EventTypeStub", (), { # type: ignore[misc,assignment] "ROOM_MESSAGE": "m.room.message", "REACTION": "m.reaction", - "ROOM_ENCRYPTED": "m.room.encrypted", "ROOM_NAME": "m.room.name", - }) + "ROOM_ENCRYPTED": "m.room.encrypted", "ROOM_NAME": "m.room.name"}) PresenceState = type("_PresenceStateStub", (), { # type: ignore[misc,assignment] - "ONLINE": "online", "OFFLINE": "offline", "UNAVAILABLE": "unavailable", - }) + "ONLINE": "online", "OFFLINE": "offline", "UNAVAILABLE": "unavailable"}) RoomCreatePreset = type("_RoomCreatePresetStub", (), { # type: ignore[misc,assignment] "PRIVATE": "private_chat", "PUBLIC": "public_chat", "TRUSTED_PRIVATE": "trusted_private_chat", }) @@ -62,8 +59,7 @@ except ImportError: from gateway.config import Platform, PlatformConfig from gateway.platforms.base import ( gateway_trust_env, BasePlatformAdapter, MessageEvent, MessageType, ProcessingOutcome, - SendResult, resolve_proxy_url, proxy_kwargs_for_aiohttp, _ssrf_redirect_guard, -) + SendResult, resolve_proxy_url, proxy_kwargs_for_aiohttp, _ssrf_redirect_guard) from gateway.platforms.helpers import ThreadParticipationTracker logger = logging.getLogger(__name__) @@ -129,8 +125,7 @@ def _matrix_transcode_voice_to_ogg(path: str) -> Optional[str]: result = _run_media_tool( [ffmpeg, "-v", "error", "-y", "-i", str(path), "-acodec", "libopus", "-ac", "1", "-b:a", "48k", "-vbr", "on", "-application", "voip", "-compression_level", "10", ogg_path], - timeout=30, - ) + timeout=30) if result.returncode == 0 and os.path.getsize(ogg_path) > 0: return ogg_path except Exception: @@ -230,10 +225,9 @@ class _MatrixHtmlSanitizer(HTMLParser): """Allowlist sanitizer for Matrix-compatible formatted HTML.""" _ALLOWED_TAGS = { - "a", "b", "blockquote", "br", "code", "del", "em", "h1", "h2", "h3", - "h4", "h5", "h6", "hr", "i", "li", "ol", "p", "pre", "s", "strike", - "strong", "table", "tbody", "td", "th", "thead", "tr", "ul", - } + "a", "b", "blockquote", "br", "code", "del", "em", "h1", "h2", "h3", "h4", "h5", "h6", "hr", + "i", "li", "ol", "p", "pre", "s", "strike", "strong", "table", "tbody", "td", "th", "thead", + "tr", "ul"} _VOID_TAGS = {"br", "hr"} def __init__(self) -> None: @@ -379,52 +373,26 @@ def _resolve_max_message_length(config) -> int: return max(500, min(value, MATRIX_MAX_MESSAGE_LENGTH_CEILING)) -# Back-compat alias for callers/tests that import the module constant. -MAX_MESSAGE_LENGTH = DEFAULT_MAX_MESSAGE_LENGTH +MAX_MESSAGE_LENGTH = DEFAULT_MAX_MESSAGE_LENGTH # back-compat alias for importers of the module constant -# E2EE store dir is resolved per adapter in connect() (``_resolve_store_dir``), NOT at -# module scope: the multiplex gateway imports this once, and a module constant would make -# every profile's Olm identity collide in one crypto.db. +# E2EE store dir is resolved per adapter in connect() (``_resolve_store_dir``), NOT at module scope: +# the multiplex gateway imports this once and a module constant would collide every profile's Olm +# identity in one crypto.db. from hermes_constants import get_hermes_dir as _get_hermes_dir -# Grace period: ignore messages older than this many seconds before startup. -_STARTUP_GRACE_SECONDS = 5 +_STARTUP_GRACE_SECONDS = 5 # ignore messages older than this many seconds before startup _OUTBOUND_MENTION_RE = re.compile(r"(? bool: """True when an m.image body is just the uploaded filename (no caption) — not user text.""" @@ -488,8 +456,7 @@ def _create_matrix_session(proxy_url: str | None): logger.warning( "aiohttp_socks not installed — SOCKS proxy %s ignored. " "Run: pip install aiohttp-socks", - proxy_url, - ) + proxy_url) return aiohttp.ClientSession(trust_env=gateway_trust_env()) return aiohttp.ClientSession(proxy=proxy_url) @@ -620,17 +587,13 @@ def _handle_generated_matrix_recovery_key(mxid: str, recovery_key: str) -> None: "Matrix: bootstrapped cross-signing for %s. A new recovery key was " "written to %s with mode 0600. Move it to your secret store and set " "MATRIX_RECOVERY_KEY for future restarts.", - mxid, - output_path, - ) + mxid, output_path) else: logger.warning( - "Matrix: bootstrapped cross-signing for %s. A new recovery key was " - "generated but will not be logged. Set MATRIX_RECOVERY_KEY_OUTPUT_FILE " - "to write it once with mode 0600, or configure MATRIX_RECOVERY_KEY " - "from your Matrix client before future restarts.", - mxid, - ) + "Matrix: bootstrapped cross-signing for %s. A new recovery key was generated but will " + "not be logged. Set MATRIX_RECOVERY_KEY_OUTPUT_FILE to write it once with mode 0600, " + "or configure MATRIX_RECOVERY_KEY from your Matrix client before future restarts.", + mxid) def _scoped_recovery_key() -> str: @@ -729,20 +692,16 @@ def ensure_matrix_deps() -> bool: "TrustState": TrustState, "UserID": UserID} if not ensure_and_bind("platform.matrix", _import, globals(), prompt=False): logger.warning( - "Matrix: required packages not installed (%s). " - "Run: pip install 'mautrix[encryption]' asyncpg aiosqlite " - "Markdown aiohttp-socks", - ", ".join(missing) if missing else "platform.matrix", - ) + "Matrix: required packages not installed (%s). Run: pip install " + "'mautrix[encryption]' asyncpg aiosqlite Markdown aiohttp-socks", + ", ".join(missing) if missing else "platform.matrix") return False e2ee_mode = _resolve_e2ee_mode() if e2ee_mode == "required" and not _check_e2ee_deps(): logger.error( - "Matrix: E2EE is required but dependencies are missing. %s. " - "Without this, encrypted rooms will not work. " - "Set MATRIX_E2EE_MODE=off to disable E2EE.", - _E2EE_INSTALL_HINT, - ) + "Matrix: E2EE is required but dependencies are missing. %s. Without this, encrypted " + "rooms will not work. Set MATRIX_E2EE_MODE=off to disable E2EE.", + _E2EE_INSTALL_HINT) return False if e2ee_mode == "optional" and not _check_e2ee_deps(): logger.warning("Matrix: E2EE optional but dependencies are missing. %s", _E2EE_INSTALL_HINT) @@ -776,8 +735,7 @@ class _CryptoStateStore: return None try: from mautrix.types import ( - EventType as _ET, RoomEncryptionStateEventContent as _Enc, RoomID as _RID, - ) + EventType as _ET, RoomEncryptionStateEventContent as _Enc, RoomID as _RID) raw = await client.get_state_event(_RID(room_id), _ET.ROOM_ENCRYPTION) except Exception as exc: logger.debug("Matrix: homeserver encryption-info query failed for %s: %s", room_id, exc) @@ -803,10 +761,7 @@ class MatrixAdapter(BasePlatformAdapter): supports_code_blocks = True # Matrix renders fenced code blocks (HTML/markdown) splits_long_messages = True # send() chunks via truncate_message(max_message_length) - - # Clients reserve typed "/" for local commands; "!command" always reaches Hermes. - typed_command_prefix = "!" - + typed_command_prefix = "!" # clients reserve typed "/" for local commands; "!command" always reaches Hermes # Class-level defaults keep object.__new__-built test instances working. max_message_length = DEFAULT_MAX_MESSAGE_LENGTH _split_threshold = DEFAULT_MAX_MESSAGE_LENGTH - 100 @@ -819,8 +774,7 @@ class MatrixAdapter(BasePlatformAdapter): @property def _crypto_db_path(self) -> Path: - store_dir = self._store_dir or _get_hermes_dir("platforms/matrix/store", "matrix/store") - return store_dir / "crypto.db" + return (self._store_dir or _get_hermes_dir("platforms/matrix/store", "matrix/store")) / "crypto.db" def __init__(self, config: PlatformConfig): super().__init__(config, Platform.MATRIX) @@ -1462,25 +1416,21 @@ class MatrixAdapter(BasePlatformAdapter): user_id = self._user_id or getattr(self._client, "mxid", "") or "" device_id = self._device_id or getattr(self._client, "device_id", "") or "" return { - "platform": "matrix", - "homeserver": self._homeserver, + "platform": "matrix", "homeserver": self._homeserver, "auth": { "access_token_present": token_present, "password_present": bool(self._password), "token_preview": "***" if token_present else "", "user_id": user_id, "device_id_present": bool(device_id), - "device_id_preview": _redact_matrix_value(device_id), - }, + "device_id_preview": _redact_matrix_value(device_id)}, "sync": { - "connected": self._client is not None, - "joined_room_count": len(self._joined_rooms), - "last_sync_age_seconds": max(0.0, now - self._last_sync_ts) if self._last_sync_ts else None, + "connected": self._client is not None, "joined_room_count": len(self._joined_rooms), + "last_sync_age_seconds": max(0.0, now - self._last_sync_ts) if self._last_sync_ts else None }, "e2ee": { "mode": self._e2ee_mode, "enabled": bool(self._encryption), "deps_available": _check_e2ee_deps(), "crypto_store_path": str(self._crypto_db_path), - "recovery_key_configured": bool(_scoped_recovery_key().strip()), - }, + "recovery_key_configured": bool(_scoped_recovery_key().strip())}, "policy": { "allowed_user_count": len(self._allowed_user_ids), "allowed_room_count": len(self._allowed_room_ids), @@ -1489,10 +1439,8 @@ class MatrixAdapter(BasePlatformAdapter): "free_response_room_count": len(self._free_rooms), "allow_room_mentions": self._allow_room_mentions, "process_notices": self._process_notices, - "allow_public_rooms": _env_truthy("MATRIX_ALLOW_PUBLIC_ROOMS"), - }, - "media": {"max_media_bytes": self._max_media_bytes}, - } + "allow_public_rooms": _env_truthy("MATRIX_ALLOW_PUBLIC_ROOMS")}, + "media": {"max_media_bytes": self._max_media_bytes}} # ---- optional overrides ---- @@ -2720,7 +2668,7 @@ class MatrixAdapter(BasePlatformAdapter): async def _get_room_member_count(self, room_id: str) -> Optional[int]: """state_store first (cached), then a direct joined_members API query.""" - state_store = (getattr(self._client, "state_store", None) if self._client else None) + state_store = getattr(self._client, "state_store", None) if self._client else None if state_store: try: members = await state_store.get_members(room_id) @@ -2728,7 +2676,7 @@ class MatrixAdapter(BasePlatformAdapter): return len(members) except Exception: pass - client = getattr(self, "_client", None) + client = getattr(self, "_client", None) # object.__new__-built test doubles may lack it if client is not None and hasattr(client, "joined_members"): try: resp = await client.joined_members(room_id) @@ -2746,15 +2694,12 @@ class MatrixAdapter(BasePlatformAdapter): event = await self._client.get_state_event(RoomID(room_id), event_type) except Exception: return None - value = self._state_event_value(event, key) - return value.strip() if value and value.strip() else None + value = (self._state_event_value(event, key) or "").strip() + return value or None @staticmethod def _room_server_name(room_id: str) -> Optional[str]: - if ":" not in room_id: - return None - server = room_id.rsplit(":", 1)[-1].strip() - return server or None + return (room_id.rsplit(":", 1)[-1].strip() or None) if ":" in room_id else None def _invalidate_room_identities(self, room_id: str | None = None) -> None: """Drop one cached room identity (or all when *room_id* is None).""" @@ -2776,11 +2721,8 @@ class MatrixAdapter(BasePlatformAdapter): async def _resolve_room_identity(self, room_id: str, *, force_refresh: bool = False) -> MatrixRoomIdentity: """Resolve room identity; member count is the primary DM signal (see below).""" cached = self._room_identities.get(room_id) - cached_at = self._room_identity_cached_at.get(room_id, 0.0) - cache_fresh = ( - self._room_identity_ttl_seconds <= 0 - or time.monotonic() - cached_at <= self._room_identity_ttl_seconds - ) + ttl = self._room_identity_ttl_seconds + cache_fresh = ttl <= 0 or time.monotonic() - self._room_identity_cached_at.get(room_id, 0.0) <= ttl if cached is not None and cache_fresh and not force_refresh: return cached room_name = await self._get_room_state_value(room_id, "m.room.name", "name") @@ -2792,16 +2734,12 @@ class MatrixAdapter(BasePlatformAdapter): # <=2 members is necessarily a DM regardless of m.direct/name (clients auto-name DMs # like "Alice & Bot"); fall back to m.direct + unnamed only when the count is unknown. is_likely_dm = (member_count is not None and member_count <= 2) or (is_direct and not has_explicit_name) - conflict = bool(is_direct and has_explicit_name and (member_count is None or member_count > 2)) - chat_type = "dm" if is_likely_dm else "room" - display_name = room_name or canonical_alias or room_id identity = MatrixRoomIdentity( - room_id=room_id, room_name=room_name, room_topic=room_topic, - canonical_alias=canonical_alias, server_name=self._room_server_name(room_id), - joined_member_count=member_count, is_direct_account_data=is_direct, - display_name=display_name, has_explicit_name=has_explicit_name, chat_type=chat_type, - conflict=conflict, - ) + room_id=room_id, room_name=room_name, room_topic=room_topic, canonical_alias=canonical_alias, + server_name=self._room_server_name(room_id), joined_member_count=member_count, + is_direct_account_data=is_direct, display_name=room_name or canonical_alias or room_id, + has_explicit_name=has_explicit_name, chat_type="dm" if is_likely_dm else "room", + conflict=bool(is_direct and has_explicit_name and (member_count is None or member_count > 2))) self._cache_room_identity(room_id, identity) return identity @@ -2831,7 +2769,7 @@ class MatrixAdapter(BasePlatformAdapter): if dm_data is None: return dm_room_ids: Set[str] = set() - for user_id, rooms in dm_data.items(): + for rooms in dm_data.values(): if isinstance(rooms, list): dm_room_ids.update(str(r) for r in rooms if isinstance(r, str)) self._dm_rooms = {rid: (rid in dm_room_ids) for rid in self._joined_rooms} @@ -2840,16 +2778,11 @@ class MatrixAdapter(BasePlatformAdapter): async def _record_dm_room(self, room_id: str, inviter: str) -> None: """Persist a room as DM in m.direct account data after an invite. - When the bot account has never been used for DMs, ``m.direct`` is - absent (404). This method fetches the current mapping (if any), - appends *room_id* under the *inviter*'s entry, and writes it back - so that subsequent ``_refresh_dm_cache`` calls treat the room as a - DM without requiring manual ``m.direct`` setup. + ``m.direct`` is absent (404) until the account has had a DM; fetch the current mapping (if + any), append *room_id* under *inviter*, write it back so ``_refresh_dm_cache`` sees the DM. """ if not self._client: return - - # m.direct may not exist yet (404) — start fresh. dm_data: Dict[str, list] = await self._fetch_m_direct(require_dict=True) or {} rooms_for_user = dm_data.get(inviter, []) if not isinstance(rooms_for_user, list): @@ -2866,21 +2799,17 @@ class MatrixAdapter(BasePlatformAdapter): self._dm_rooms[room_id] = True self._invalidate_room_identities(room_id) - # ------------------------------------------------------------------ - # Mention detection helpers - # ------------------------------------------------------------------ + # ---- outbound formatting / mention helpers ---- def _build_text_message_content(self, text: str, msgtype: str = "m.text") -> Dict[str, Any]: """Build Matrix text content with HTML and outbound mention metadata.""" msg_content: Dict[str, Any] = {"msgtype": msgtype, "body": text} mention_user_ids = self._extract_outbound_mentions(text) - room_mentioned = self._allow_room_mentions and self._has_outbound_room_mention(text) if mention_user_ids: msg_content["m.mentions"] = {"user_ids": mention_user_ids} - if room_mentioned: + if self._allow_room_mentions and self._has_outbound_room_mention(text): msg_content.setdefault("m.mentions", {})["room"] = True - html_source = self._inject_outbound_mention_links(text) - html = self._markdown_to_html(html_source) + html = self._markdown_to_html(self._inject_outbound_mention_links(text)) if html and html != text: msg_content["format"] = "org.matrix.custom.html" msg_content["formatted_body"] = html @@ -2888,8 +2817,7 @@ class MatrixAdapter(BasePlatformAdapter): def _apply_relation_metadata( self, msg_content: Dict[str, Any], *, reply_to: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - ) -> None: + metadata: Optional[Dict[str, Any]] = None) -> None: """Apply Matrix reply/thread relation metadata to an outbound payload.""" thread_id = str((metadata or {}).get("thread_id") or "") if reply_to: @@ -2906,14 +2834,7 @@ class MatrixAdapter(BasePlatformAdapter): def _extract_outbound_mentions(self, text: str) -> list[str]: """Return unique Matrix user IDs mentioned in outbound text.""" protected, _ = self._protect_outbound_mention_regions(text) - seen: Set[str] = set() - mentions: list[str] = [] - for match in _OUTBOUND_MENTION_RE.finditer(protected): - user_id = match.group(1) - if user_id not in seen: - seen.add(user_id) - mentions.append(user_id) - return mentions + return list(dict.fromkeys(m.group(1) for m in _OUTBOUND_MENTION_RE.finditer(protected))) def _has_outbound_room_mention(self, text: str) -> bool: """Return True when outbound text contains @room outside protected spans.""" @@ -2925,9 +2846,7 @@ class MatrixAdapter(BasePlatformAdapter): if not text: return text protected, placeholders = self._protect_outbound_mention_regions(text) - linked = _OUTBOUND_MENTION_RE.sub( - lambda match: f"[{match.group(1)}](https://matrix.to/#/{match.group(1)})", protected - ) + linked = _OUTBOUND_MENTION_RE.sub(lambda m: f"[{m.group(1)}](https://matrix.to/#/{m.group(1)})", protected) for idx, original in enumerate(placeholders): linked = linked.replace(f"\x00MENTION_PROTECTED{idx}\x00", original) return linked @@ -2946,8 +2865,7 @@ class MatrixAdapter(BasePlatformAdapter): return protected, placeholders def _is_bot_mentioned( - self, body: str, formatted_body: Optional[str] = None, mention_user_ids: Optional[list] = None - ) -> bool: + self, body: str, formatted_body: Optional[str] = None, mention_user_ids: Optional[list] = None) -> bool: """True if the bot is mentioned; ``m.mentions.user_ids`` (MSC3952) is authoritative even when the body has no ``@bot`` text (pills may live only in formatted_body).""" if mention_user_ids and self._user_id and self._user_id in mention_user_ids: @@ -2956,12 +2874,15 @@ class MatrixAdapter(BasePlatformAdapter): return False if self._user_id and self._user_id in body: return True - if self._user_id and ":" in self._user_id: - localpart = self._user_id.split(":")[0].lstrip("@") - if localpart and re.search(r"\b" + re.escape(localpart) + r"\b", body, re.IGNORECASE): - return True + localpart = self._user_localpart() + if localpart and re.search(r"\b" + re.escape(localpart) + r"\b", body, re.IGNORECASE): + return True return bool(formatted_body and self._user_id and f"matrix.to/#/{self._user_id}" in formatted_body) + def _user_localpart(self) -> str: + """``@bot:server`` -> ``bot``; empty when the user ID has no server part.""" + return self._user_id.split(":")[0].lstrip("@") if self._user_id and ":" in self._user_id else "" + def _strip_mention(self, body: str) -> str: """Strip explicit ``@user:server`` / ``@localpart`` tokens only — never bare localpart words, or "Hermes Agent" would become "Agent".""" @@ -2969,10 +2890,9 @@ class MatrixAdapter(BasePlatformAdapter): return "" if self._user_id: body = body.replace(self._user_id, "") - if self._user_id and ":" in self._user_id: - localpart = self._user_id.split(":")[0].lstrip("@") - if localpart: - body = re.sub(r'(? str: """Get a user's display name in a room, falling back to user_id.""" - state_store = (getattr(self._client, "state_store", None) if self._client else None) + state_store = getattr(self._client, "state_store", None) if self._client else None if state_store: try: member = await state_store.get_member(room_id, user_id) @@ -2996,8 +2916,7 @@ class MatrixAdapter(BasePlatformAdapter): """Convert mxc://server/media_id to an HTTP download URL.""" if not mxc_url.startswith("mxc://"): return mxc_url - parts = mxc_url[6:] # strip mxc:// - return f"{self._homeserver}/_matrix/client/v1/media/download/{parts}" + return f"{self._homeserver}/_matrix/client/v1/media/download/{mxc_url[6:]}" def _markdown_to_html(self, text: str) -> str: """Markdown → org.matrix.custom.html via ``markdown`` when installed, else the regex fallback.""" @@ -3020,8 +2939,7 @@ class MatrixAdapter(BasePlatformAdapter): def _sanitize_link_url(url: str) -> str: """Sanitize a URL for use in an href attribute.""" stripped = url.strip() - scheme = stripped.split(":", 1)[0].lower().strip() if ":" in stripped else "" - if scheme in {"javascript", "data", "vbscript"}: + if ":" in stripped and stripped.split(":", 1)[0].lower().strip() in {"javascript", "data", "vbscript"}: return "" return stripped.replace('"', """) @@ -3041,28 +2959,18 @@ class MatrixAdapter(BasePlatformAdapter): result = re.sub( r"```(\w*)\n(.*?)```", lambda m: _protect_html( - f'
'
-                f"{_html_escape(m.group(2))}
" - if m.group(1) - else f"
{_html_escape(m.group(2))}
" - ), - text, - flags=re.DOTALL, - ) + f'
{_html_escape(m.group(2))}
' + if m.group(1) else f"
{_html_escape(m.group(2))}
"), + text, flags=re.DOTALL) result = re.sub(r"`([^`\n]+)`", lambda m: _protect_html(f"{_html_escape(m.group(1))}"), result) # Protect markdown links before escaping. result = re.sub( r"\[([^\]]+)\]\(([^)]+)\)", lambda m: _protect_html( - '{}'.format(MatrixAdapter._sanitize_link_url(m.group(2)), _html_escape(m.group(1))) - ), - result, - ) - parts = re.split(r"(\x00PROTECTED\d+\x00)", result) - for idx, part in enumerate(parts): - if not part.startswith("\x00PROTECTED"): - parts[idx] = _html_escape(part) - result = "".join(parts) + f'{_html_escape(m.group(1))}'), + result) + result = "".join( + p if p.startswith("\x00PROTECTED") else _html_escape(p) for p in re.split(r"(\x00PROTECTED\d+\x00)", result)) # Block-level transforms (line-oriented): hr, headers, blockquote, lists. lines = result.split("\n") out_lines: list = [] @@ -3083,12 +2991,7 @@ class MatrixAdapter(BasePlatformAdapter): bq_lines = [] while i < len(lines) and _is_bq_line(lines[i]): ln = lines[i] - if ln.startswith("> "): - bq_lines.append(ln[5:]) - elif ln.startswith("> "): - bq_lines.append(ln[2:]) - else: - bq_lines.append("") + bq_lines.append(ln[5:] if ln.startswith("> ") else ln[2:] if ln.startswith("> ") else "") i += 1 out_lines.append(f"
{'
'.join(bq_lines)}
") continue @@ -3098,8 +3001,7 @@ class MatrixAdapter(BasePlatformAdapter): while i < len(lines) and re.match(item_re, lines[i]): items.append(re.match(item_re, lines[i]).group(1)) i += 1 - li = "".join(f"
  • {item}
  • " for item in items) - out_lines.append(f"<{tag}>{li}") + out_lines.append(f"<{tag}>{''.join(f'
  • {item}
  • ' for item in items)}") break else: out_lines.append(line) @@ -3108,8 +3010,7 @@ class MatrixAdapter(BasePlatformAdapter): for pattern, repl in ( (r"\*\*(.+?)\*\*", r"\1"), (r"__(.+?)__", r"\1"), (r"\*(.+?)\*", r"\1"), (r"(?\1"), - (r"~~(.+?)~~", r"\1"), - ): + (r"~~(.+?)~~", r"\1")): result = re.sub(pattern, repl, result, flags=re.DOTALL) result = re.sub(r"\n", "
    \n", result) result = re.sub(r"
    \n( None: except Exception as exc: print_warning( "Install failed — run manually: pip install " - "'mautrix[encryption]' asyncpg aiosqlite Markdown aiohttp-socks" - ) + "'mautrix[encryption]' asyncpg aiosqlite Markdown aiohttp-socks") print_info(f" Error: {exc}") except ImportError: try: @@ -3236,8 +3129,7 @@ def interactive_setup() -> None: else: print_warning( f"Install failed — run manually: uv pip install " - f"'{matrix_pkg}' asyncpg aiosqlite Markdown aiohttp-socks" - ) + f"'{matrix_pkg}' asyncpg aiosqlite Markdown aiohttp-socks") print_info("🔒 Security: Restrict who can use your bot") print_info(" Matrix user IDs look like @username:server") allowed_users = prompt("Allowed user IDs (comma-separated, leave empty for open access)") @@ -3260,14 +3152,10 @@ def interactive_setup() -> None: _YAML_LOWER_KEYS = ( ("require_mention", "MATRIX_REQUIRE_MENTION"), ("process_notices", "MATRIX_PROCESS_NOTICES"), ("session_scope", "MATRIX_SESSION_SCOPE"), ("auto_thread", "MATRIX_AUTO_THREAD"), - ("dm_mention_threads", "MATRIX_DM_MENTION_THREADS"), -) + ("dm_mention_threads", "MATRIX_DM_MENTION_THREADS")) _YAML_LIST_KEYS = ( - ("allowed_users", "MATRIX_ALLOWED_USERS"), - ("free_response_rooms", "MATRIX_FREE_RESPONSE_ROOMS"), - ("allowed_rooms", "MATRIX_ALLOWED_ROOMS"), - ("ignore_user_patterns", "MATRIX_IGNORE_USER_PATTERNS"), -) + ("allowed_users", "MATRIX_ALLOWED_USERS"), ("free_response_rooms", "MATRIX_FREE_RESPONSE_ROOMS"), + ("allowed_rooms", "MATRIX_ALLOWED_ROOMS"), ("ignore_user_patterns", "MATRIX_IGNORE_USER_PATTERNS")) def _apply_yaml_config(yaml_cfg: dict, matrix_cfg: dict) -> dict | None: @@ -3296,12 +3184,8 @@ def _is_connected(config) -> bool: extra = getattr(config, "extra", {}) or {} import hermes_cli.gateway as gateway_mod homeserver = extra.get("homeserver") or gateway_mod.get_env_value("MATRIX_HOMESERVER") or "" - token = ( - getattr(config, "token", None) - or gateway_mod.get_env_value("MATRIX_ACCESS_TOKEN") - or gateway_mod.get_env_value("MATRIX_PASSWORD") - or "" - ) + token = (getattr(config, "token", None) or gateway_mod.get_env_value("MATRIX_ACCESS_TOKEN") + or gateway_mod.get_env_value("MATRIX_PASSWORD") or "") return bool(str(homeserver).strip() and str(token).strip()) @@ -3320,5 +3204,4 @@ def register(ctx) -> None: apply_yaml_config_fn=_apply_yaml_config, allowed_users_env="MATRIX_ALLOWED_USERS", allow_all_env="MATRIX_ALLOW_ALL_USERS", cron_deliver_env_var="MATRIX_HOME_ROOM", standalone_sender_fn=_standalone_send, max_message_length=DEFAULT_MAX_MESSAGE_LENGTH, - emoji="🔐", allow_update_command=True, - ) + emoji="🔐", allow_update_command=True)