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

Commit 3f32bdf

Browse files
committed
Support MSC3874.
1 parent c75836f commit 3f32bdf

File tree

9 files changed

+290
-21
lines changed

9 files changed

+290
-21
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: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2942,6 +2942,11 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
29422942
self.third_tok = self.login("third", "test")
29432943
self.helper.join(room=self.room_id, user=self.third_user_id, tok=self.third_tok)
29442944

2945+
# Store a token which is after all the room creation events.
2946+
self.from_token = self.get_success(
2947+
self.hs.get_event_sources().get_current_token_for_pagination(self.room_id)
2948+
)
2949+
29452950
# An initial event with a relation from second user.
29462951
res = self.helper.send_event(
29472952
room_id=self.room_id,
@@ -2950,7 +2955,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
29502955
tok=self.tok,
29512956
)
29522957
self.event_id_1 = res["event_id"]
2953-
self.helper.send_event(
2958+
res = self.helper.send_event(
29542959
room_id=self.room_id,
29552960
type="m.reaction",
29562961
content={
@@ -2962,6 +2967,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
29622967
},
29632968
tok=self.second_tok,
29642969
)
2970+
self.event_id_annotation = res["event_id"]
29652971

29662972
# Another event with a relation from third user.
29672973
res = self.helper.send_event(
@@ -2971,7 +2977,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
29712977
tok=self.tok,
29722978
)
29732979
self.event_id_2 = res["event_id"]
2974-
self.helper.send_event(
2980+
res = self.helper.send_event(
29752981
room_id=self.room_id,
29762982
type="m.reaction",
29772983
content={
@@ -2982,20 +2988,25 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
29822988
},
29832989
tok=self.third_tok,
29842990
)
2991+
self.event_id_reference = res["event_id"]
29852992

29862993
# An event with no relations.
2987-
self.helper.send_event(
2994+
res = self.helper.send_event(
29882995
room_id=self.room_id,
29892996
type=EventTypes.Message,
29902997
content={"msgtype": "m.text", "body": "No relations"},
29912998
tok=self.tok,
29922999
)
3000+
self.event_id_none = res["event_id"]
29933001

29943002
def _filter_messages(self, filter: JsonDict) -> List[JsonDict]:
29953003
"""Make a request to /messages with a filter, returns the chunk of events."""
3004+
from_token = self.get_success(
3005+
self.from_token.to_string(self.hs.get_datastores().main)
3006+
)
29963007
channel = self.make_request(
29973008
"GET",
2998-
"/rooms/%s/messages?filter=%s&dir=b" % (self.room_id, json.dumps(filter)),
3009+
f"/rooms/{self.room_id}/messages?filter={json.dumps(filter)}&dir=f&from={from_token}",
29993010
access_token=self.tok,
30003011
)
30013012
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
@@ -3059,6 +3070,77 @@ def test_filter_relation_senders_and_type(self) -> None:
30593070
self.assertEqual(len(chunk), 1, chunk)
30603071
self.assertEqual(chunk[0]["event_id"], self.event_id_1)
30613072

3073+
def test_filter_rel_types(self) -> None:
3074+
# Messages which are annotations.
3075+
filter = {"org.matrix.msc3874.rel_types": [RelationTypes.ANNOTATION]}
3076+
chunk = self._filter_messages(filter)
3077+
self.assertEqual(len(chunk), 1, chunk)
3078+
self.assertEqual(chunk[0]["event_id"], self.event_id_annotation)
3079+
3080+
# Messages which are references.
3081+
filter = {"org.matrix.msc3874.rel_types": [RelationTypes.REFERENCE]}
3082+
chunk = self._filter_messages(filter)
3083+
self.assertEqual(len(chunk), 1, chunk)
3084+
self.assertEqual(chunk[0]["event_id"], self.event_id_reference)
3085+
3086+
# Messages which are either annotations or references.
3087+
filter = {
3088+
"org.matrix.msc3874.rel_types": [
3089+
RelationTypes.ANNOTATION,
3090+
RelationTypes.REFERENCE,
3091+
]
3092+
}
3093+
chunk = self._filter_messages(filter)
3094+
self.assertEqual(len(chunk), 2, chunk)
3095+
self.assertCountEqual(
3096+
[c["event_id"] for c in chunk],
3097+
[self.event_id_annotation, self.event_id_reference],
3098+
)
3099+
3100+
def test_filter_not_rel_types(self) -> None:
3101+
# Messages which are not annotations.
3102+
filter = {"org.matrix.msc3874.not_rel_types": [RelationTypes.ANNOTATION]}
3103+
chunk = self._filter_messages(filter)
3104+
self.assertEqual(len(chunk), 4, chunk)
3105+
event_ids = [ev["event_id"] for ev in chunk]
3106+
self.assertEqual(
3107+
event_ids,
3108+
[
3109+
self.event_id_1,
3110+
self.event_id_2,
3111+
self.event_id_reference,
3112+
self.event_id_none,
3113+
],
3114+
)
3115+
3116+
# Messages which are not references.
3117+
filter = {"org.matrix.msc3874.not_rel_types": [RelationTypes.REFERENCE]}
3118+
chunk = self._filter_messages(filter)
3119+
self.assertEqual(len(chunk), 4, chunk)
3120+
event_ids = [ev["event_id"] for ev in chunk]
3121+
self.assertEqual(
3122+
event_ids,
3123+
[
3124+
self.event_id_1,
3125+
self.event_id_annotation,
3126+
self.event_id_2,
3127+
self.event_id_none,
3128+
],
3129+
)
3130+
3131+
# Messages which are neither annotations or references.
3132+
filter = {
3133+
"org.matrix.msc3874.not_rel_types": [
3134+
RelationTypes.ANNOTATION,
3135+
RelationTypes.REFERENCE,
3136+
]
3137+
}
3138+
chunk = self._filter_messages(filter)
3139+
event_ids = [ev["event_id"] for ev in chunk]
3140+
self.assertEqual(
3141+
event_ids, [self.event_id_1, self.event_id_2, self.event_id_none]
3142+
)
3143+
30623144

30633145
class ContextTestCase(unittest.HomeserverTestCase):
30643146

0 commit comments

Comments
 (0)