fix(matrix): gate require_mention / allowed rooms before downloading inbound media
`_handle_media_message` fetched the mxc:// payload (`_download_and_cache_media`) before `_build_inbound_event` ran `_resolve_message_context`, so an unmentioned or non-allowlisted room's m.image/m.file was pulled onto the host and then dropped. Resolve the context first and hand it to `_build_inbound_event` via a new `ctx` kwarg so the read receipt / thread mark are still applied once. Same class as the Teams fix in this PR (review follow-up). One invariant test: unmentioned group media → 0 downloads, mentioned → 1.
This commit is contained in:
@@ -2087,10 +2087,13 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||
|
||||
async def _build_inbound_event(
|
||||
self, room_id: str, sender: str, event_id: str, body: str, source_content: dict, relates_to: dict,
|
||||
**extra) -> Optional[MessageEvent]:
|
||||
ctx: Optional[tuple] = None, **extra) -> Optional[MessageEvent]:
|
||||
"""Gate + normalise an inbound event into a MessageEvent (None => drop). Text body may
|
||||
still change (reply-fallback strip); ``extra`` carries media fields / message_type."""
|
||||
ctx = await self._resolve_message_context(room_id, sender, event_id, body, source_content, relates_to)
|
||||
still change (reply-fallback strip); ``extra`` carries media fields / message_type.
|
||||
``ctx`` is a pre-resolved ``_resolve_message_context`` result (media path gates before
|
||||
downloading); resolving it twice would double the read receipt / thread mark."""
|
||||
if ctx is None:
|
||||
ctx = await self._resolve_message_context(room_id, sender, event_id, body, source_content, relates_to)
|
||||
if ctx is None:
|
||||
return None
|
||||
body, _is_dm, _chat_type, _thread_id, display_name, source = ctx
|
||||
@@ -2154,6 +2157,11 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||
return
|
||||
is_encrypted_media = bool(file_content and isinstance(file_content, dict) and file_content.get("url"))
|
||||
msg_type, media_type, is_voice_message = self._classify_inbound_media(msgtype, event_mimetype, source_content)
|
||||
# Gate (require_mention / allowed rooms) BEFORE the download: an unmentioned or
|
||||
# non-allowlisted room must not pull media onto the host only to drop it.
|
||||
ctx = await self._resolve_message_context(room_id, sender, event_id, body, source_content, relates_to)
|
||||
if ctx is None:
|
||||
return
|
||||
# Cache locally so downstream tools get a real file path.
|
||||
cached_path = None
|
||||
if url:
|
||||
@@ -2167,7 +2175,7 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||
http_url = self._mxc_to_http(url) if url and not is_encrypted_media else ""
|
||||
media_urls = [cached_path] if cached_path else ([http_url] if http_url else None)
|
||||
msg_event = await self._build_inbound_event(
|
||||
room_id, sender, event_id, body, source_content, relates_to, message_type=msg_type,
|
||||
room_id, sender, event_id, body, source_content, relates_to, ctx=ctx, message_type=msg_type,
|
||||
media_urls=media_urls, media_types=[media_type] if media_urls else None, media_msgtype=msgtype)
|
||||
if msg_event is not None:
|
||||
await self.handle_message(msg_event)
|
||||
|
||||
51
tests/gateway/test_matrix_media_gate_before_download.py
Normal file
51
tests/gateway/test_matrix_media_gate_before_download.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Matrix must gate (require_mention / allowed rooms) BEFORE downloading inbound media.
|
||||
|
||||
An unmentioned ``m.image`` in a gated group room used to be fetched from the homeserver
|
||||
(``_download_and_cache_media``) and only then dropped by ``_resolve_message_context``.
|
||||
The invariant: a dropped media event performs zero downloads; a mentioned one still does.
|
||||
"""
|
||||
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_adapter(monkeypatch):
|
||||
monkeypatch.setenv("MATRIX_REQUIRE_MENTION", "true")
|
||||
monkeypatch.setenv("MATRIX_AUTO_THREAD", "false")
|
||||
from gateway.config import PlatformConfig
|
||||
from plugins.platforms.matrix.adapter import MatrixAdapter
|
||||
|
||||
adapter = MatrixAdapter(PlatformConfig(
|
||||
enabled=True, token="syt_test_token",
|
||||
extra={"homeserver": "https://matrix.example.org", "user_id": "@hermes:example.org"}))
|
||||
adapter._startup_ts = time.time() - 10
|
||||
adapter.handle_message = AsyncMock()
|
||||
adapter._client = None
|
||||
adapter._resolve_room_identity = AsyncMock(return_value=SimpleNamespace(
|
||||
display_name="Group Room", room_topic=None, server_name="example.org", chat_type="group"))
|
||||
adapter._is_dm_room = AsyncMock(return_value=False)
|
||||
adapter._download_and_cache_media = AsyncMock(return_value="/tmp/cached.png")
|
||||
return adapter
|
||||
|
||||
|
||||
def _image_event(body):
|
||||
return SimpleNamespace(
|
||||
sender="@alice:example.org", event_id="$img1", room_id="!group:example.org",
|
||||
timestamp=int(time.time() * 1000),
|
||||
content={"body": body, "msgtype": "m.image", "url": "mxc://example.org/abc",
|
||||
"info": {"mimetype": "image/png", "size": 1024}})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("body, downloads, dispatched", [
|
||||
("photo.png", 0, 0), # unmentioned group media: never fetched
|
||||
("@hermes:example.org look", 1, 1), # mentioned: fetched and dispatched
|
||||
])
|
||||
async def test_unmentioned_group_media_is_not_downloaded(monkeypatch, body, downloads, dispatched):
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
await adapter._on_room_message(_image_event(body))
|
||||
assert adapter._download_and_cache_media.await_count == downloads
|
||||
assert adapter.handle_message.await_count == dispatched
|
||||
Reference in New Issue
Block a user