Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 4283bd1

Browse files
authored
Support filtering the /messages API by relation type (MSC3874). (#14148)
Gated behind an experimental configuration flag.
1 parent 6b24235 commit 4283bd1

File tree

9 files changed

+212
-177
lines changed

9 files changed

+212
-177
lines changed

changelog.d/14148.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for [MSC3874](https://github.com/matrix-org/matrix-spec-proposals/pull/3874).

synapse/api/filtering.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from synapse.api.constants import EduTypes, EventContentFields
3737
from synapse.api.errors import SynapseError
3838
from synapse.api.presence import UserPresenceState
39-
from synapse.events import EventBase
39+
from synapse.events import EventBase, relation_from_event
4040
from synapse.types import JsonDict, RoomID, UserID
4141

4242
if TYPE_CHECKING:
@@ -53,6 +53,12 @@
5353
# check types are valid event types
5454
"types": {"type": "array", "items": {"type": "string"}},
5555
"not_types": {"type": "array", "items": {"type": "string"}},
56+
# MSC3874, filtering /messages.
57+
"org.matrix.msc3874.rel_types": {"type": "array", "items": {"type": "string"}},
58+
"org.matrix.msc3874.not_rel_types": {
59+
"type": "array",
60+
"items": {"type": "string"},
61+
},
5662
},
5763
}
5864

@@ -334,8 +340,15 @@ def __init__(self, hs: "HomeServer", filter_json: JsonDict):
334340
self.labels = filter_json.get("org.matrix.labels", None)
335341
self.not_labels = filter_json.get("org.matrix.not_labels", [])
336342

337-
self.related_by_senders = self.filter_json.get("related_by_senders", None)
338-
self.related_by_rel_types = self.filter_json.get("related_by_rel_types", None)
343+
self.related_by_senders = filter_json.get("related_by_senders", None)
344+
self.related_by_rel_types = filter_json.get("related_by_rel_types", None)
345+
346+
# For compatibility with _check_fields.
347+
self.rel_types = None
348+
self.not_rel_types = []
349+
if hs.config.experimental.msc3874_enabled:
350+
self.rel_types = filter_json.get("org.matrix.msc3874.rel_types", None)
351+
self.not_rel_types = filter_json.get("org.matrix.msc3874.not_rel_types", [])
339352

340353
def filters_all_types(self) -> bool:
341354
return "*" in self.not_types
@@ -386,11 +399,19 @@ def _check(self, event: FilterEvent) -> bool:
386399
# check if there is a string url field in the content for filtering purposes
387400
labels = content.get(EventContentFields.LABELS, [])
388401

402+
# Check if the event has a relation.
403+
rel_type = None
404+
if isinstance(event, EventBase):
405+
relation = relation_from_event(event)
406+
if relation:
407+
rel_type = relation.rel_type
408+
389409
field_matchers = {
390410
"rooms": lambda v: room_id == v,
391411
"senders": lambda v: sender == v,
392412
"types": lambda v: _matches_wildcard(ev_type, v),
393413
"labels": lambda v: v in labels,
414+
"rel_types": lambda v: rel_type == v,
394415
}
395416

396417
result = self._check_fields(field_matchers)

synapse/config/experimental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
117117
self.msc3882_token_timeout = self.parse_duration(
118118
experimental.get("msc3882_token_timeout", "5m")
119119
)
120+
121+
# MSC3874: Filtering /messages with rel_types / not_rel_types.
122+
self.msc3874_enabled: bool = experimental.get("msc3874_enabled", False)

synapse/rest/client/versions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
114114
"org.matrix.msc3882": self.config.experimental.msc3882_enabled,
115115
# Adds support for remotely enabling/disabling pushers, as per MSC3881
116116
"org.matrix.msc3881": self.config.experimental.msc3881_enabled,
117+
# Adds support for filtering /messages by event relation.
118+
"org.matrix.msc3874": self.config.experimental.msc3874_enabled,
117119
},
118120
},
119121
)

synapse/storage/databases/main/stream.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,24 @@ def filter_to_clause(event_filter: Optional[Filter]) -> Tuple[str, List[str]]:
357357
)
358358
args.extend(event_filter.related_by_rel_types)
359359

360+
if event_filter.rel_types:
361+
clauses.append(
362+
"(%s)"
363+
% " OR ".join(
364+
"event_relation.relation_type = ?" for _ in event_filter.rel_types
365+
)
366+
)
367+
args.extend(event_filter.rel_types)
368+
369+
if event_filter.not_rel_types:
370+
clauses.append(
371+
"((%s) OR event_relation.relation_type IS NULL)"
372+
% " AND ".join(
373+
"event_relation.relation_type != ?" for _ in event_filter.not_rel_types
374+
)
375+
)
376+
args.extend(event_filter.not_rel_types)
377+
360378
return " AND ".join(clauses), args
361379

362380

@@ -1278,8 +1296,8 @@ def _paginate_room_events_txn(
12781296
# Multiple labels could cause the same event to appear multiple times.
12791297
needs_distinct = True
12801298

1281-
# If there is a filter on relation_senders and relation_types join to the
1282-
# relations table.
1299+
# If there is a relation_senders and relation_types filter join to the
1300+
# relations table to get events related to the current event.
12831301
if event_filter and (
12841302
event_filter.related_by_senders or event_filter.related_by_rel_types
12851303
):
@@ -1294,6 +1312,13 @@ def _paginate_room_events_txn(
12941312
LEFT JOIN events AS related_event ON (relation.event_id = related_event.event_id)
12951313
"""
12961314

1315+
# If there is a not_rel_types filter join to the relations table to get
1316+
# the event's relation information.
1317+
if event_filter and (event_filter.rel_types or event_filter.not_rel_types):
1318+
join_clause += """
1319+
LEFT JOIN event_relations AS event_relation USING (event_id)
1320+
"""
1321+
12971322
if needs_distinct:
12981323
select_keywords += " DISTINCT"
12991324

tests/api/test_filtering.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def MockEvent(**kwargs):
3535
kwargs["event_id"] = "fake_event_id"
3636
if "type" not in kwargs:
3737
kwargs["type"] = "fake_type"
38+
if "content" not in kwargs:
39+
kwargs["content"] = {}
3840
return make_event_from_dict(kwargs)
3941

4042

@@ -357,6 +359,66 @@ def test_filter_not_labels(self):
357359

358360
self.assertTrue(Filter(self.hs, definition)._check(event))
359361

362+
@unittest.override_config({"experimental_features": {"msc3874_enabled": True}})
363+
def test_filter_rel_type(self):
364+
definition = {"org.matrix.msc3874.rel_types": ["m.thread"]}
365+
event = MockEvent(
366+
sender="@foo:bar",
367+
type="m.room.message",
368+
room_id="!secretbase:unknown",
369+
content={},
370+
)
371+
372+
self.assertFalse(Filter(self.hs, definition)._check(event))
373+
374+
event = MockEvent(
375+
sender="@foo:bar",
376+
type="m.room.message",
377+
room_id="!secretbase:unknown",
378+
content={"m.relates_to": {"event_id": "$abc", "rel_type": "m.reference"}},
379+
)
380+
381+
self.assertFalse(Filter(self.hs, definition)._check(event))
382+
383+
event = MockEvent(
384+
sender="@foo:bar",
385+
type="m.room.message",
386+
room_id="!secretbase:unknown",
387+
content={"m.relates_to": {"event_id": "$abc", "rel_type": "m.thread"}},
388+
)
389+
390+
self.assertTrue(Filter(self.hs, definition)._check(event))
391+
392+
@unittest.override_config({"experimental_features": {"msc3874_enabled": True}})
393+
def test_filter_not_rel_type(self):
394+
definition = {"org.matrix.msc3874.not_rel_types": ["m.thread"]}
395+
event = MockEvent(
396+
sender="@foo:bar",
397+
type="m.room.message",
398+
room_id="!secretbase:unknown",
399+
content={"m.relates_to": {"event_id": "$abc", "rel_type": "m.thread"}},
400+
)
401+
402+
self.assertFalse(Filter(self.hs, definition)._check(event))
403+
404+
event = MockEvent(
405+
sender="@foo:bar",
406+
type="m.room.message",
407+
room_id="!secretbase:unknown",
408+
content={},
409+
)
410+
411+
self.assertTrue(Filter(self.hs, definition)._check(event))
412+
413+
event = MockEvent(
414+
sender="@foo:bar",
415+
type="m.room.message",
416+
room_id="!secretbase:unknown",
417+
content={"m.relates_to": {"event_id": "$abc", "rel_type": "m.reference"}},
418+
)
419+
420+
self.assertTrue(Filter(self.hs, definition)._check(event))
421+
360422
def test_filter_presence_match(self):
361423
user_filter_json = {"presence": {"types": ["m.*"]}}
362424
filter_id = self.get_success(
@@ -456,7 +518,6 @@ def test_filter_rooms(self):
456518

457519
self.assertEqual(filtered_room_ids, ["!allowed:example.com"])
458520

459-
@unittest.override_config({"experimental_features": {"msc3440_enabled": True}})
460521
def test_filter_relations(self):
461522
events = [
462523
# An event without a relation.

tests/rest/client/test_relations.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1677,7 +1677,6 @@ def test_redact_parent_annotation(self) -> None:
16771677
{"chunk": [{"type": "m.reaction", "key": "👍", "count": 1}]},
16781678
)
16791679

1680-
@unittest.override_config({"experimental_features": {"msc3440_enabled": True}})
16811680
def test_redact_parent_thread(self) -> None:
16821681
"""
16831682
Test that thread replies are still available when the root event is redacted.

tests/rest/client/test_rooms.py

Lines changed: 8 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
EventTypes,
3636
Membership,
3737
PublicRoomsFilterFields,
38-
RelationTypes,
3938
RoomTypes,
4039
)
4140
from synapse.api.errors import Codes, HttpResponseException
@@ -50,6 +49,7 @@
5049

5150
from tests import unittest
5251
from tests.http.server._base import make_request_with_cancellation_test
52+
from tests.storage.test_stream import PaginationTestCase
5353
from tests.test_utils import make_awaitable
5454

5555
PATH_PREFIX = b"/_matrix/client/api/v1"
@@ -2915,149 +2915,20 @@ def _send_labelled_messages_in_room(self) -> str:
29152915
return event_id
29162916

29172917

2918-
class RelationsTestCase(unittest.HomeserverTestCase):
2919-
servlets = [
2920-
synapse.rest.admin.register_servlets_for_client_rest_resource,
2921-
room.register_servlets,
2922-
login.register_servlets,
2923-
]
2924-
2925-
def default_config(self) -> Dict[str, Any]:
2926-
config = super().default_config()
2927-
config["experimental_features"] = {"msc3440_enabled": True}
2928-
return config
2929-
2930-
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
2931-
self.user_id = self.register_user("test", "test")
2932-
self.tok = self.login("test", "test")
2933-
self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
2934-
2935-
self.second_user_id = self.register_user("second", "test")
2936-
self.second_tok = self.login("second", "test")
2937-
self.helper.join(
2938-
room=self.room_id, user=self.second_user_id, tok=self.second_tok
2939-
)
2940-
2941-
self.third_user_id = self.register_user("third", "test")
2942-
self.third_tok = self.login("third", "test")
2943-
self.helper.join(room=self.room_id, user=self.third_user_id, tok=self.third_tok)
2944-
2945-
# An initial event with a relation from second user.
2946-
res = self.helper.send_event(
2947-
room_id=self.room_id,
2948-
type=EventTypes.Message,
2949-
content={"msgtype": "m.text", "body": "Message 1"},
2950-
tok=self.tok,
2951-
)
2952-
self.event_id_1 = res["event_id"]
2953-
self.helper.send_event(
2954-
room_id=self.room_id,
2955-
type="m.reaction",
2956-
content={
2957-
"m.relates_to": {
2958-
"rel_type": RelationTypes.ANNOTATION,
2959-
"event_id": self.event_id_1,
2960-
"key": "👍",
2961-
}
2962-
},
2963-
tok=self.second_tok,
2964-
)
2965-
2966-
# Another event with a relation from third user.
2967-
res = self.helper.send_event(
2968-
room_id=self.room_id,
2969-
type=EventTypes.Message,
2970-
content={"msgtype": "m.text", "body": "Message 2"},
2971-
tok=self.tok,
2972-
)
2973-
self.event_id_2 = res["event_id"]
2974-
self.helper.send_event(
2975-
room_id=self.room_id,
2976-
type="m.reaction",
2977-
content={
2978-
"m.relates_to": {
2979-
"rel_type": RelationTypes.REFERENCE,
2980-
"event_id": self.event_id_2,
2981-
}
2982-
},
2983-
tok=self.third_tok,
2984-
)
2985-
2986-
# An event with no relations.
2987-
self.helper.send_event(
2988-
room_id=self.room_id,
2989-
type=EventTypes.Message,
2990-
content={"msgtype": "m.text", "body": "No relations"},
2991-
tok=self.tok,
2992-
)
2993-
2994-
def _filter_messages(self, filter: JsonDict) -> List[JsonDict]:
2918+
class RelationsTestCase(PaginationTestCase):
2919+
def _filter_messages(self, filter: JsonDict) -> List[str]:
29952920
"""Make a request to /messages with a filter, returns the chunk of events."""
2921+
from_token = self.get_success(
2922+
self.from_token.to_string(self.hs.get_datastores().main)
2923+
)
29962924
channel = self.make_request(
29972925
"GET",
2998-
"/rooms/%s/messages?filter=%s&dir=b" % (self.room_id, json.dumps(filter)),
2926+
f"/rooms/{self.room_id}/messages?filter={json.dumps(filter)}&dir=f&from={from_token}",
29992927
access_token=self.tok,
30002928
)
30012929
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
30022930

3003-
return channel.json_body["chunk"]
3004-
3005-
def test_filter_relation_senders(self) -> None:
3006-
# Messages which second user reacted to.
3007-
filter = {"related_by_senders": [self.second_user_id]}
3008-
chunk = self._filter_messages(filter)
3009-
self.assertEqual(len(chunk), 1, chunk)
3010-
self.assertEqual(chunk[0]["event_id"], self.event_id_1)
3011-
3012-
# Messages which third user reacted to.
3013-
filter = {"related_by_senders": [self.third_user_id]}
3014-
chunk = self._filter_messages(filter)
3015-
self.assertEqual(len(chunk), 1, chunk)
3016-
self.assertEqual(chunk[0]["event_id"], self.event_id_2)
3017-
3018-
# Messages which either user reacted to.
3019-
filter = {"related_by_senders": [self.second_user_id, self.third_user_id]}
3020-
chunk = self._filter_messages(filter)
3021-
self.assertEqual(len(chunk), 2, chunk)
3022-
self.assertCountEqual(
3023-
[c["event_id"] for c in chunk], [self.event_id_1, self.event_id_2]
3024-
)
3025-
3026-
def test_filter_relation_type(self) -> None:
3027-
# Messages which have annotations.
3028-
filter = {"related_by_rel_types": [RelationTypes.ANNOTATION]}
3029-
chunk = self._filter_messages(filter)
3030-
self.assertEqual(len(chunk), 1, chunk)
3031-
self.assertEqual(chunk[0]["event_id"], self.event_id_1)
3032-
3033-
# Messages which have references.
3034-
filter = {"related_by_rel_types": [RelationTypes.REFERENCE]}
3035-
chunk = self._filter_messages(filter)
3036-
self.assertEqual(len(chunk), 1, chunk)
3037-
self.assertEqual(chunk[0]["event_id"], self.event_id_2)
3038-
3039-
# Messages which have either annotations or references.
3040-
filter = {
3041-
"related_by_rel_types": [
3042-
RelationTypes.ANNOTATION,
3043-
RelationTypes.REFERENCE,
3044-
]
3045-
}
3046-
chunk = self._filter_messages(filter)
3047-
self.assertEqual(len(chunk), 2, chunk)
3048-
self.assertCountEqual(
3049-
[c["event_id"] for c in chunk], [self.event_id_1, self.event_id_2]
3050-
)
3051-
3052-
def test_filter_relation_senders_and_type(self) -> None:
3053-
# Messages which second user reacted to.
3054-
filter = {
3055-
"related_by_senders": [self.second_user_id],
3056-
"related_by_rel_types": [RelationTypes.ANNOTATION],
3057-
}
3058-
chunk = self._filter_messages(filter)
3059-
self.assertEqual(len(chunk), 1, chunk)
3060-
self.assertEqual(chunk[0]["event_id"], self.event_id_1)
2931+
return [ev["event_id"] for ev in channel.json_body["chunk"]]
30612932

30622933

30632934
class ContextTestCase(unittest.HomeserverTestCase):

0 commit comments

Comments
 (0)