From 3d9e13bb789762481a8569a7470b26c0f272165d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 3 Mar 2022 09:40:58 -0500 Subject: [PATCH 01/12] Support stable and unstable thread relations. --- synapse/api/constants.py | 4 +++- synapse/events/utils.py | 4 +++- synapse/handlers/message.py | 5 ++++- synapse/storage/databases/main/events.py | 5 ++++- synapse/storage/databases/main/relations.py | 10 ++++++---- tests/rest/client/test_relations.py | 1 + 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 36ace7c6134f..b0c08a074d83 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -178,7 +178,9 @@ class RelationTypes: ANNOTATION: Final = "m.annotation" REPLACE: Final = "m.replace" REFERENCE: Final = "m.reference" - THREAD: Final = "io.element.thread" + THREAD: Final = "m.thread" + # TODO Remove this in Synapse >= v1.57.0. + UNSTABLE_THREAD: Final = "io.element.thread" class LimitBlockingTypes: diff --git a/synapse/events/utils.py b/synapse/events/utils.py index ee34cb46e437..d528d278bb8a 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -515,11 +515,13 @@ def _inject_bundled_aggregations( thread.latest_event, serialized_latest_event, thread.latest_edit ) - serialized_aggregations[RelationTypes.THREAD] = { + thread_summary = { "latest_event": serialized_latest_event, "count": thread.count, "current_user_participated": thread.current_user_participated, } + serialized_aggregations[RelationTypes.THREAD] = thread_summary + serialized_aggregations[RelationTypes.UNSTABLE_THREAD] = thread_summary # Include the bundled aggregations in the event. if serialized_aggregations: diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 0799ec9a84df..f9544fe7fb83 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1079,7 +1079,10 @@ async def _validate_event_relation(self, event: EventBase) -> None: raise SynapseError(400, "Can't send same reaction twice") # Don't attempt to start a thread if the parent event is a relation. - elif relation_type == RelationTypes.THREAD: + elif ( + relation_type == RelationTypes.THREAD + or relation_type == RelationTypes.UNSTABLE_THREAD + ): if await self.store.event_includes_relation(relates_to): raise SynapseError( 400, "Cannot start threads from an event with a relation" diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index 1dc83aa5e3a6..1beab5af92f9 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -1811,7 +1811,10 @@ def _handle_event_relations( if rel_type == RelationTypes.REPLACE: txn.call_after(self.store.get_applicable_edit.invalidate, (parent_id,)) - if rel_type == RelationTypes.THREAD: + if ( + rel_type == RelationTypes.THREAD + or rel_type == RelationTypes.UNSTABLE_THREAD + ): txn.call_after( self.store.get_thread_summary.invalidate, (parent_id, event.room_id) ) diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py index 36aa1092f602..0ce2bb66fc22 100644 --- a/synapse/storage/databases/main/relations.py +++ b/synapse/storage/databases/main/relations.py @@ -514,7 +514,7 @@ def _get_thread_summaries_txn( AND parent.room_id = child.room_id WHERE %s - AND relation_type = ? + AND (relation_type = ? OR relation_type = ?) ORDER BY child.topological_ordering DESC, child.stream_ordering DESC """ @@ -522,6 +522,7 @@ def _get_thread_summaries_txn( txn.database_engine, "relates_to_id", event_ids ) args.append(RelationTypes.THREAD) + args.append(RelationTypes.UNSTABLE_THREAD) txn.execute(sql % (clause,), args) latest_event_ids = {} @@ -543,7 +544,7 @@ def _get_thread_summaries_txn( AND parent.room_id = child.room_id WHERE %s - AND relation_type = ? + AND (relation_type = ? OR relation_type = ?) GROUP BY parent.event_id """ @@ -553,6 +554,7 @@ def _get_thread_summaries_txn( txn.database_engine, "relates_to_id", latest_event_ids.keys() ) args.append(RelationTypes.THREAD) + args.append(RelationTypes.UNSTABLE_THREAD) txn.execute(sql % (clause,), args) counts = dict(cast(List[Tuple[str, int]], txn.fetchall())) @@ -617,14 +619,14 @@ def _get_thread_summary_txn(txn: LoggingTransaction) -> Set[str]: AND parent.room_id = child.room_id WHERE %s - AND relation_type = ? + AND (relation_type = ? OR relation_type = ?) AND child.sender = ? """ clause, args = make_in_list_sql_clause( txn.database_engine, "relates_to_id", event_ids ) - args.extend((RelationTypes.THREAD, user_id)) + args.extend((RelationTypes.THREAD, RelationTypes.UNSTABLE_THREAD, user_id)) txn.execute(sql % (clause,), args) return {row[0] for row in txn.fetchall()} diff --git a/tests/rest/client/test_relations.py b/tests/rest/client/test_relations.py index a40a5de3991c..ac3243c4591b 100644 --- a/tests/rest/client/test_relations.py +++ b/tests/rest/client/test_relations.py @@ -597,6 +597,7 @@ def assert_bundle(event_json: JsonDict) -> None: RelationTypes.ANNOTATION, RelationTypes.REFERENCE, RelationTypes.THREAD, + RelationTypes.UNSTABLE_THREAD, ), ) From ae46d1b2ff3b9bf8e5c59d6f29b48aaf858981c1 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 3 Mar 2022 10:59:42 -0500 Subject: [PATCH 02/12] Support stable and unstable filtering by relations. --- synapse/api/filtering.py | 21 +++++++++++++-------- synapse/storage/databases/main/stream.py | 18 ++++++++++-------- tests/rest/client/test_rooms.py | 18 ++++++++---------- tests/storage/test_stream.py | 20 +++++++++----------- 4 files changed, 40 insertions(+), 37 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index cb532d723828..8189796058a3 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -88,7 +88,9 @@ "org.matrix.labels": {"type": "array", "items": {"type": "string"}}, "org.matrix.not_labels": {"type": "array", "items": {"type": "string"}}, # MSC3440, filtering by event relations. + "related_by_senders": {"type": "array", "items": {"type": "string"}}, "io.element.relation_senders": {"type": "array", "items": {"type": "string"}}, + "related_by_rel_types": {"type": "array", "items": {"type": "string"}}, "io.element.relation_types": {"type": "array", "items": {"type": "string"}}, }, } @@ -322,15 +324,18 @@ def __init__(self, hs: "HomeServer", filter_json: JsonDict): # and not supported, but that would involve modifying the JSON schema # based on the homeserver configuration. if hs.config.experimental.msc3440_enabled: - self.relation_senders = self.filter_json.get( - "io.element.relation_senders", None + # Fallback to the unstable prefix if the stable version is not given. + self.related_by_senders = self.filter_json.get( + "related_by_senders", + self.filter_json.get("io.element.relation_senders", None), ) - self.relation_types = self.filter_json.get( - "io.element.relation_types", None + self.related_by_rel_types = self.filter_json.get( + "related_by_rel_types", + self.filter_json.get("io.element.relation_types", None), ) else: - self.relation_senders = None - self.relation_types = None + self.related_by_senders = None + self.related_by_rel_types = None def filters_all_types(self) -> bool: return "*" in self.not_types @@ -461,7 +466,7 @@ async def _check_event_relations( event_ids = [event.event_id for event in events if isinstance(event, EventBase)] # type: ignore[attr-defined] event_ids_to_keep = set( await self._store.events_have_relations( - event_ids, self.relation_senders, self.relation_types + event_ids, self.related_by_senders, self.related_by_rel_types ) ) @@ -474,7 +479,7 @@ async def _check_event_relations( async def filter(self, events: Iterable[FilterEvent]) -> List[FilterEvent]: result = [event for event in events if self._check(event)] - if self.relation_senders or self.relation_types: + if self.related_by_senders or self.related_by_rel_types: return await self._check_event_relations(result) return result diff --git a/synapse/storage/databases/main/stream.py b/synapse/storage/databases/main/stream.py index a898f847e7d5..39e1efe37348 100644 --- a/synapse/storage/databases/main/stream.py +++ b/synapse/storage/databases/main/stream.py @@ -325,21 +325,23 @@ def filter_to_clause(event_filter: Optional[Filter]) -> Tuple[str, List[str]]: args.extend(event_filter.labels) # Filter on relation_senders / relation types from the joined tables. - if event_filter.relation_senders: + if event_filter.related_by_senders: clauses.append( "(%s)" % " OR ".join( - "related_event.sender = ?" for _ in event_filter.relation_senders + "related_event.sender = ?" for _ in event_filter.related_by_senders ) ) - args.extend(event_filter.relation_senders) + args.extend(event_filter.related_by_senders) - if event_filter.relation_types: + if event_filter.related_by_rel_types: clauses.append( "(%s)" - % " OR ".join("relation_type = ?" for _ in event_filter.relation_types) + % " OR ".join( + "relation_type = ?" for _ in event_filter.related_by_rel_types + ) ) - args.extend(event_filter.relation_types) + args.extend(event_filter.related_by_rel_types) return " AND ".join(clauses), args @@ -1203,7 +1205,7 @@ def _paginate_room_events_txn( # If there is a filter on relation_senders and relation_types join to the # relations table. if event_filter and ( - event_filter.relation_senders or event_filter.relation_types + event_filter.related_by_senders or event_filter.related_by_rel_types ): # Filtering by relations could cause the same event to appear multiple # times (since there's no limit on the number of relations to an event). @@ -1211,7 +1213,7 @@ def _paginate_room_events_txn( join_clause += """ LEFT JOIN event_relations AS relation ON (event.event_id = relation.relates_to_id) """ - if event_filter.relation_senders: + if event_filter.related_by_senders: join_clause += """ LEFT JOIN events AS related_event ON (relation.event_id = related_event.event_id) """ diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index 37866ee330f3..3a9617d6da8e 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -2141,21 +2141,19 @@ def _filter_messages(self, filter: JsonDict) -> List[JsonDict]: def test_filter_relation_senders(self) -> None: # Messages which second user reacted to. - filter = {"io.element.relation_senders": [self.second_user_id]} + filter = {"related_by_senders": [self.second_user_id]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0]["event_id"], self.event_id_1) # Messages which third user reacted to. - filter = {"io.element.relation_senders": [self.third_user_id]} + filter = {"related_by_senders": [self.third_user_id]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0]["event_id"], self.event_id_2) # Messages which either user reacted to. - filter = { - "io.element.relation_senders": [self.second_user_id, self.third_user_id] - } + filter = {"related_by_senders": [self.second_user_id, self.third_user_id]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 2, chunk) self.assertCountEqual( @@ -2164,20 +2162,20 @@ def test_filter_relation_senders(self) -> None: def test_filter_relation_type(self) -> None: # Messages which have annotations. - filter = {"io.element.relation_types": [RelationTypes.ANNOTATION]} + filter = {"related_by_rel_types": [RelationTypes.ANNOTATION]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0]["event_id"], self.event_id_1) # Messages which have references. - filter = {"io.element.relation_types": [RelationTypes.REFERENCE]} + filter = {"related_by_rel_types": [RelationTypes.REFERENCE]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0]["event_id"], self.event_id_2) # Messages which have either annotations or references. filter = { - "io.element.relation_types": [ + "related_by_rel_types": [ RelationTypes.ANNOTATION, RelationTypes.REFERENCE, ] @@ -2191,8 +2189,8 @@ def test_filter_relation_type(self) -> None: def test_filter_relation_senders_and_type(self) -> None: # Messages which second user reacted to. filter = { - "io.element.relation_senders": [self.second_user_id], - "io.element.relation_types": [RelationTypes.ANNOTATION], + "related_by_senders": [self.second_user_id], + "related_by_rel_types": [RelationTypes.ANNOTATION], } chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) diff --git a/tests/storage/test_stream.py b/tests/storage/test_stream.py index 6a1cf3305455..eaa0d7d749b7 100644 --- a/tests/storage/test_stream.py +++ b/tests/storage/test_stream.py @@ -129,21 +129,19 @@ def _filter_messages(self, filter: JsonDict) -> List[EventBase]: def test_filter_relation_senders(self): # Messages which second user reacted to. - filter = {"io.element.relation_senders": [self.second_user_id]} + filter = {"related_by_senders": [self.second_user_id]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0].event_id, self.event_id_1) # Messages which third user reacted to. - filter = {"io.element.relation_senders": [self.third_user_id]} + filter = {"related_by_senders": [self.third_user_id]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0].event_id, self.event_id_2) # Messages which either user reacted to. - filter = { - "io.element.relation_senders": [self.second_user_id, self.third_user_id] - } + filter = {"related_by_senders": [self.second_user_id, self.third_user_id]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 2, chunk) self.assertCountEqual( @@ -152,20 +150,20 @@ def test_filter_relation_senders(self): def test_filter_relation_type(self): # Messages which have annotations. - filter = {"io.element.relation_types": [RelationTypes.ANNOTATION]} + filter = {"related_by_rel_types": [RelationTypes.ANNOTATION]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0].event_id, self.event_id_1) # Messages which have references. - filter = {"io.element.relation_types": [RelationTypes.REFERENCE]} + filter = {"related_by_rel_types": [RelationTypes.REFERENCE]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0].event_id, self.event_id_2) # Messages which have either annotations or references. filter = { - "io.element.relation_types": [ + "related_by_rel_types": [ RelationTypes.ANNOTATION, RelationTypes.REFERENCE, ] @@ -179,8 +177,8 @@ def test_filter_relation_type(self): def test_filter_relation_senders_and_type(self): # Messages which second user reacted to. filter = { - "io.element.relation_senders": [self.second_user_id], - "io.element.relation_types": [RelationTypes.ANNOTATION], + "related_by_senders": [self.second_user_id], + "related_by_rel_types": [RelationTypes.ANNOTATION], } chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) @@ -201,7 +199,7 @@ def test_duplicate_relation(self): tok=self.second_tok, ) - filter = {"io.element.relation_senders": [self.second_user_id]} + filter = {"related_by_senders": [self.second_user_id]} chunk = self._filter_messages(filter) self.assertEqual(len(chunk), 1, chunk) self.assertEqual(chunk[0].event_id, self.event_id_1) From b01d7653043e7c779ddb1dceaf8e356faf132c34 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 3 Mar 2022 11:02:37 -0500 Subject: [PATCH 03/12] Remove experimental config flag. --- synapse/api/filtering.py | 25 ++++------ synapse/config/experimental.py | 2 - synapse/rest/client/versions.py | 2 +- synapse/storage/databases/main/relations.py | 55 +++++++-------------- tests/rest/client/test_relations.py | 7 +-- 5 files changed, 29 insertions(+), 62 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 8189796058a3..e86c830a32a6 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -320,22 +320,15 @@ def __init__(self, hs: "HomeServer", filter_json: JsonDict): self.labels = filter_json.get("org.matrix.labels", None) self.not_labels = filter_json.get("org.matrix.not_labels", []) - # Ideally these would be rejected at the endpoint if they were provided - # and not supported, but that would involve modifying the JSON schema - # based on the homeserver configuration. - if hs.config.experimental.msc3440_enabled: - # Fallback to the unstable prefix if the stable version is not given. - self.related_by_senders = self.filter_json.get( - "related_by_senders", - self.filter_json.get("io.element.relation_senders", None), - ) - self.related_by_rel_types = self.filter_json.get( - "related_by_rel_types", - self.filter_json.get("io.element.relation_types", None), - ) - else: - self.related_by_senders = None - self.related_by_rel_types = None + # Fallback to the unstable prefix if the stable version is not given. + self.related_by_senders = self.filter_json.get( + "related_by_senders", + self.filter_json.get("io.element.relation_senders", None), + ) + self.related_by_rel_types = self.filter_json.get( + "related_by_rel_types", + self.filter_json.get("io.element.relation_types", None), + ) def filters_all_types(self) -> bool: return "*" in self.not_types diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 41338b39df21..d8694d12b6f7 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -24,8 +24,6 @@ class ExperimentalConfig(Config): def read_config(self, config: JsonDict, **kwargs): experimental = config.get("experimental_features") or {} - # MSC3440 (thread relation) - self.msc3440_enabled: bool = experimental.get("msc3440_enabled", False) # MSC3666: including bundled relations in /search. self.msc3666_enabled: bool = experimental.get("msc3666_enabled", False) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 2e5d0e4e2258..8aaa6efa774d 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -100,7 +100,7 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]: # Adds support for jump to date endpoints (/timestamp_to_event) as per MSC3030 "org.matrix.msc3030": self.config.experimental.msc3030_enabled, # Adds support for thread relations, per MSC3440. - "org.matrix.msc3440": self.config.experimental.msc3440_enabled, + "org.matrix.msc3440": True, }, }, ) diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py index 0ce2bb66fc22..36725f8c7d92 100644 --- a/synapse/storage/databases/main/relations.py +++ b/synapse/storage/databases/main/relations.py @@ -32,12 +32,7 @@ from synapse.api.constants import RelationTypes from synapse.events import EventBase from synapse.storage._base import SQLBaseStore -from synapse.storage.database import ( - DatabasePool, - LoggingDatabaseConnection, - LoggingTransaction, - make_in_list_sql_clause, -) +from synapse.storage.database import LoggingTransaction, make_in_list_sql_clause from synapse.storage.databases.main.stream import generate_pagination_where_clause from synapse.storage.engines import PostgresEngine from synapse.storage.relations import AggregationPaginationToken, PaginationChunk @@ -45,7 +40,6 @@ from synapse.util.caches.descriptors import cached, cachedList if TYPE_CHECKING: - from synapse.server import HomeServer from synapse.storage.databases.main import DataStore logger = logging.getLogger(__name__) @@ -81,16 +75,6 @@ def __bool__(self) -> bool: class RelationsWorkerStore(SQLBaseStore): - def __init__( - self, - database: DatabasePool, - db_conn: LoggingDatabaseConnection, - hs: "HomeServer", - ): - super().__init__(database, db_conn, hs) - - self._msc3440_enabled = hs.config.experimental.msc3440_enabled - @cached(tree=True) async def get_relations_for_event( self, @@ -832,26 +816,23 @@ async def get_bundled_aggregations( results.setdefault(event_id, BundledAggregations()).replace = edit # Fetch thread summaries. - if self._msc3440_enabled: - summaries = await self._get_thread_summaries(seen_event_ids) - # Only fetch participated for a limited selection based on what had - # summaries. - participated = await self._get_threads_participated( - summaries.keys(), user_id - ) - for event_id, summary in summaries.items(): - if summary: - thread_count, latest_thread_event, edit = summary - results.setdefault( - event_id, BundledAggregations() - ).thread = _ThreadAggregation( - latest_event=latest_thread_event, - latest_edit=edit, - count=thread_count, - # If there's a thread summary it must also exist in the - # participated dictionary. - current_user_participated=participated[event_id], - ) + summaries = await self._get_thread_summaries(seen_event_ids) + # Only fetch participated for a limited selection based on what had + # summaries. + participated = await self._get_threads_participated(summaries.keys(), user_id) + for event_id, summary in summaries.items(): + if summary: + thread_count, latest_thread_event, edit = summary + results.setdefault( + event_id, BundledAggregations() + ).thread = _ThreadAggregation( + latest_event=latest_thread_event, + latest_edit=edit, + count=thread_count, + # If there's a thread summary it must also exist in the + # participated dictionary. + current_user_participated=participated[event_id], + ) return results diff --git a/tests/rest/client/test_relations.py b/tests/rest/client/test_relations.py index ac3243c4591b..7a759a29a0b4 100644 --- a/tests/rest/client/test_relations.py +++ b/tests/rest/client/test_relations.py @@ -547,9 +547,7 @@ def test_aggregation_must_be_annotation(self) -> None: ) self.assertEqual(400, channel.code, channel.json_body) - @unittest.override_config( - {"experimental_features": {"msc3440_enabled": True, "msc3666_enabled": True}} - ) + @unittest.override_config({"experimental_features": {"msc3666_enabled": True}}) def test_bundled_aggregations(self) -> None: """ Test that annotations, references, and threads get correctly bundled. @@ -759,7 +757,6 @@ def test_aggregation_get_event_for_thread(self) -> None: }, ) - @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_ignore_invalid_room(self) -> None: """Test that we ignore invalid relations over federation.""" # Create another room and send a message in it. @@ -1066,7 +1063,6 @@ def test_edit_reply(self) -> None: {"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict ) - @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_edit_thread(self) -> None: """Test that editing a thread works.""" @@ -1384,7 +1380,6 @@ def test_redact_relation_annotation(self) -> None: chunk = self._get_aggregations() self.assertEqual(chunk, [{"type": "m.reaction", "key": "a", "count": 1}]) - @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_redact_relation_thread(self) -> None: """ Test that thread replies are properly handled after the thread reply redacted. From 10bae454ba242dfd86262ca1aa90b51798514599 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 3 Mar 2022 11:04:50 -0500 Subject: [PATCH 04/12] Newsfragment --- changelog.d/12151.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/12151.feature diff --git a/changelog.d/12151.feature b/changelog.d/12151.feature new file mode 100644 index 000000000000..18432b2da9a5 --- /dev/null +++ b/changelog.d/12151.feature @@ -0,0 +1 @@ +Support the stable identifiers from [MSC3440](https://github.com/matrix-org/matrix-doc/pull/3440): threads. From 3b0759c3d3b0512ee7846de0ce1374d9901218d3 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 8 Mar 2022 07:11:35 -0500 Subject: [PATCH 05/12] Ensure that Postgres and sqlite have the same number of parameters. --- synapse/storage/databases/main/relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py index 36725f8c7d92..c1649b0a0df7 100644 --- a/synapse/storage/databases/main/relations.py +++ b/synapse/storage/databases/main/relations.py @@ -483,7 +483,7 @@ def _get_thread_summaries_txn( AND parent.room_id = child.room_id WHERE %s - AND relation_type = ? + AND (relation_type = ? OR relation_type = ?) ORDER BY parent.event_id, child.topological_ordering DESC, child.stream_ordering DESC """ else: From 2f6071e944b89e7c159116c2d398533a29ebfcdf Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 8 Mar 2022 07:07:15 -0500 Subject: [PATCH 06/12] Revert "Remove experimental config flag." This reverts commit b01d7653043e7c779ddb1dceaf8e356faf132c34. --- synapse/api/filtering.py | 25 ++++++---- synapse/config/experimental.py | 2 + synapse/rest/client/versions.py | 2 +- synapse/storage/databases/main/relations.py | 55 ++++++++++++++------- tests/rest/client/test_relations.py | 7 ++- 5 files changed, 62 insertions(+), 29 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index e86c830a32a6..8189796058a3 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -320,15 +320,22 @@ def __init__(self, hs: "HomeServer", filter_json: JsonDict): self.labels = filter_json.get("org.matrix.labels", None) self.not_labels = filter_json.get("org.matrix.not_labels", []) - # Fallback to the unstable prefix if the stable version is not given. - self.related_by_senders = self.filter_json.get( - "related_by_senders", - self.filter_json.get("io.element.relation_senders", None), - ) - self.related_by_rel_types = self.filter_json.get( - "related_by_rel_types", - self.filter_json.get("io.element.relation_types", None), - ) + # Ideally these would be rejected at the endpoint if they were provided + # and not supported, but that would involve modifying the JSON schema + # based on the homeserver configuration. + if hs.config.experimental.msc3440_enabled: + # Fallback to the unstable prefix if the stable version is not given. + self.related_by_senders = self.filter_json.get( + "related_by_senders", + self.filter_json.get("io.element.relation_senders", None), + ) + self.related_by_rel_types = self.filter_json.get( + "related_by_rel_types", + self.filter_json.get("io.element.relation_types", None), + ) + else: + self.related_by_senders = None + self.related_by_rel_types = None def filters_all_types(self) -> bool: return "*" in self.not_types diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index d8694d12b6f7..41338b39df21 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -24,6 +24,8 @@ class ExperimentalConfig(Config): def read_config(self, config: JsonDict, **kwargs): experimental = config.get("experimental_features") or {} + # MSC3440 (thread relation) + self.msc3440_enabled: bool = experimental.get("msc3440_enabled", False) # MSC3666: including bundled relations in /search. self.msc3666_enabled: bool = experimental.get("msc3666_enabled", False) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 8aaa6efa774d..2e5d0e4e2258 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -100,7 +100,7 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]: # Adds support for jump to date endpoints (/timestamp_to_event) as per MSC3030 "org.matrix.msc3030": self.config.experimental.msc3030_enabled, # Adds support for thread relations, per MSC3440. - "org.matrix.msc3440": True, + "org.matrix.msc3440": self.config.experimental.msc3440_enabled, }, }, ) diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py index c1649b0a0df7..25924e402ee7 100644 --- a/synapse/storage/databases/main/relations.py +++ b/synapse/storage/databases/main/relations.py @@ -32,7 +32,12 @@ from synapse.api.constants import RelationTypes from synapse.events import EventBase from synapse.storage._base import SQLBaseStore -from synapse.storage.database import LoggingTransaction, make_in_list_sql_clause +from synapse.storage.database import ( + DatabasePool, + LoggingDatabaseConnection, + LoggingTransaction, + make_in_list_sql_clause, +) from synapse.storage.databases.main.stream import generate_pagination_where_clause from synapse.storage.engines import PostgresEngine from synapse.storage.relations import AggregationPaginationToken, PaginationChunk @@ -40,6 +45,7 @@ from synapse.util.caches.descriptors import cached, cachedList if TYPE_CHECKING: + from synapse.server import HomeServer from synapse.storage.databases.main import DataStore logger = logging.getLogger(__name__) @@ -75,6 +81,16 @@ def __bool__(self) -> bool: class RelationsWorkerStore(SQLBaseStore): + def __init__( + self, + database: DatabasePool, + db_conn: LoggingDatabaseConnection, + hs: "HomeServer", + ): + super().__init__(database, db_conn, hs) + + self._msc3440_enabled = hs.config.experimental.msc3440_enabled + @cached(tree=True) async def get_relations_for_event( self, @@ -816,23 +832,26 @@ async def get_bundled_aggregations( results.setdefault(event_id, BundledAggregations()).replace = edit # Fetch thread summaries. - summaries = await self._get_thread_summaries(seen_event_ids) - # Only fetch participated for a limited selection based on what had - # summaries. - participated = await self._get_threads_participated(summaries.keys(), user_id) - for event_id, summary in summaries.items(): - if summary: - thread_count, latest_thread_event, edit = summary - results.setdefault( - event_id, BundledAggregations() - ).thread = _ThreadAggregation( - latest_event=latest_thread_event, - latest_edit=edit, - count=thread_count, - # If there's a thread summary it must also exist in the - # participated dictionary. - current_user_participated=participated[event_id], - ) + if self._msc3440_enabled: + summaries = await self._get_thread_summaries(seen_event_ids) + # Only fetch participated for a limited selection based on what had + # summaries. + participated = await self._get_threads_participated( + summaries.keys(), user_id + ) + for event_id, summary in summaries.items(): + if summary: + thread_count, latest_thread_event, edit = summary + results.setdefault( + event_id, BundledAggregations() + ).thread = _ThreadAggregation( + latest_event=latest_thread_event, + latest_edit=edit, + count=thread_count, + # If there's a thread summary it must also exist in the + # participated dictionary. + current_user_participated=participated[event_id], + ) return results diff --git a/tests/rest/client/test_relations.py b/tests/rest/client/test_relations.py index 7a759a29a0b4..ac3243c4591b 100644 --- a/tests/rest/client/test_relations.py +++ b/tests/rest/client/test_relations.py @@ -547,7 +547,9 @@ def test_aggregation_must_be_annotation(self) -> None: ) self.assertEqual(400, channel.code, channel.json_body) - @unittest.override_config({"experimental_features": {"msc3666_enabled": True}}) + @unittest.override_config( + {"experimental_features": {"msc3440_enabled": True, "msc3666_enabled": True}} + ) def test_bundled_aggregations(self) -> None: """ Test that annotations, references, and threads get correctly bundled. @@ -757,6 +759,7 @@ def test_aggregation_get_event_for_thread(self) -> None: }, ) + @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_ignore_invalid_room(self) -> None: """Test that we ignore invalid relations over federation.""" # Create another room and send a message in it. @@ -1063,6 +1066,7 @@ def test_edit_reply(self) -> None: {"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict ) + @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_edit_thread(self) -> None: """Test that editing a thread works.""" @@ -1380,6 +1384,7 @@ def test_redact_relation_annotation(self) -> None: chunk = self._get_aggregations() self.assertEqual(chunk, [{"type": "m.reaction", "key": "a", "count": 1}]) + @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_redact_relation_thread(self) -> None: """ Test that thread replies are properly handled after the thread reply redacted. From 95f11e938303c057cc1b7fabdd53cc8feb77a53f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 8 Mar 2022 07:20:24 -0500 Subject: [PATCH 07/12] Do not support unstable prefixes unless config flag is set. --- synapse/api/filtering.py | 22 +++--- synapse/rest/client/versions.py | 2 +- synapse/storage/databases/main/relations.py | 75 ++++++++++++--------- tests/rest/client/test_relations.py | 7 +- 4 files changed, 56 insertions(+), 50 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 8189796058a3..27e97d6f372d 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -320,22 +320,18 @@ def __init__(self, hs: "HomeServer", filter_json: JsonDict): self.labels = filter_json.get("org.matrix.labels", None) self.not_labels = filter_json.get("org.matrix.not_labels", []) - # Ideally these would be rejected at the endpoint if they were provided - # and not supported, but that would involve modifying the JSON schema - # based on the homeserver configuration. + self.related_by_senders = self.filter_json.get("related_by_senders", None) + self.related_by_rel_types = self.filter_json.get("related_by_rel_types", None) + + # Fallback to the unstable prefix if the stable version is not given. if hs.config.experimental.msc3440_enabled: - # Fallback to the unstable prefix if the stable version is not given. - self.related_by_senders = self.filter_json.get( - "related_by_senders", - self.filter_json.get("io.element.relation_senders", None), + self.related_by_senders = self.related_by_senders or self.filter_json.get( + "io.element.relation_senders", None ) - self.related_by_rel_types = self.filter_json.get( - "related_by_rel_types", - self.filter_json.get("io.element.relation_types", None), + self.related_by_rel_types = ( + self.related_by_rel_types + or self.filter_json.get("io.element.relation_types", None) ) - else: - self.related_by_senders = None - self.related_by_rel_types = None def filters_all_types(self) -> bool: return "*" in self.not_types diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 2e5d0e4e2258..8aaa6efa774d 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -100,7 +100,7 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]: # Adds support for jump to date endpoints (/timestamp_to_event) as per MSC3030 "org.matrix.msc3030": self.config.experimental.msc3030_enabled, # Adds support for thread relations, per MSC3440. - "org.matrix.msc3440": self.config.experimental.msc3440_enabled, + "org.matrix.msc3440": True, }, }, ) diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py index 25924e402ee7..c1a7a860e3f3 100644 --- a/synapse/storage/databases/main/relations.py +++ b/synapse/storage/databases/main/relations.py @@ -499,7 +499,7 @@ def _get_thread_summaries_txn( AND parent.room_id = child.room_id WHERE %s - AND (relation_type = ? OR relation_type = ?) + AND %s ORDER BY parent.event_id, child.topological_ordering DESC, child.stream_ordering DESC """ else: @@ -514,17 +514,22 @@ def _get_thread_summaries_txn( AND parent.room_id = child.room_id WHERE %s - AND (relation_type = ? OR relation_type = ?) + AND %s ORDER BY child.topological_ordering DESC, child.stream_ordering DESC """ clause, args = make_in_list_sql_clause( txn.database_engine, "relates_to_id", event_ids ) + args.append(RelationTypes.THREAD) - args.append(RelationTypes.UNSTABLE_THREAD) + if self._msc3440_enabled: + relations_clause = "(relation_type = ? OR relation_type = ?)" + args.append(RelationTypes.UNSTABLE_THREAD) + else: + relations_clause = "relation_type = ?" - txn.execute(sql % (clause,), args) + txn.execute(sql % (clause, relations_clause), args) latest_event_ids = {} for parent_event_id, child_event_id in txn: # Only consider the latest threaded reply (by topological ordering). @@ -544,7 +549,7 @@ def _get_thread_summaries_txn( AND parent.room_id = child.room_id WHERE %s - AND (relation_type = ? OR relation_type = ?) + AND %s GROUP BY parent.event_id """ @@ -553,10 +558,15 @@ def _get_thread_summaries_txn( clause, args = make_in_list_sql_clause( txn.database_engine, "relates_to_id", latest_event_ids.keys() ) + args.append(RelationTypes.THREAD) - args.append(RelationTypes.UNSTABLE_THREAD) + if self._msc3440_enabled: + relations_clause = "(relation_type = ? OR relation_type = ?)" + args.append(RelationTypes.UNSTABLE_THREAD) + else: + relations_clause = "relation_type = ?" - txn.execute(sql % (clause,), args) + txn.execute(sql % (clause, relations_clause), args) counts = dict(cast(List[Tuple[str, int]], txn.fetchall())) return counts, latest_event_ids @@ -619,16 +629,24 @@ def _get_thread_summary_txn(txn: LoggingTransaction) -> Set[str]: AND parent.room_id = child.room_id WHERE %s - AND (relation_type = ? OR relation_type = ?) + AND %s AND child.sender = ? """ clause, args = make_in_list_sql_clause( txn.database_engine, "relates_to_id", event_ids ) - args.extend((RelationTypes.THREAD, RelationTypes.UNSTABLE_THREAD, user_id)) - txn.execute(sql % (clause,), args) + args.append(RelationTypes.THREAD) + if self._msc3440_enabled: + relations_clause = "(relation_type = ? OR relation_type = ?)" + args.append(RelationTypes.UNSTABLE_THREAD) + else: + relations_clause = "relation_type = ?" + + args.append(user_id) + + txn.execute(sql % (clause, relations_clause), args) return {row[0] for row in txn.fetchall()} participated_threads = await self.db_pool.runInteraction( @@ -832,26 +850,23 @@ async def get_bundled_aggregations( results.setdefault(event_id, BundledAggregations()).replace = edit # Fetch thread summaries. - if self._msc3440_enabled: - summaries = await self._get_thread_summaries(seen_event_ids) - # Only fetch participated for a limited selection based on what had - # summaries. - participated = await self._get_threads_participated( - summaries.keys(), user_id - ) - for event_id, summary in summaries.items(): - if summary: - thread_count, latest_thread_event, edit = summary - results.setdefault( - event_id, BundledAggregations() - ).thread = _ThreadAggregation( - latest_event=latest_thread_event, - latest_edit=edit, - count=thread_count, - # If there's a thread summary it must also exist in the - # participated dictionary. - current_user_participated=participated[event_id], - ) + summaries = await self._get_thread_summaries(seen_event_ids) + # Only fetch participated for a limited selection based on what had + # summaries. + participated = await self._get_threads_participated(summaries.keys(), user_id) + for event_id, summary in summaries.items(): + if summary: + thread_count, latest_thread_event, edit = summary + results.setdefault( + event_id, BundledAggregations() + ).thread = _ThreadAggregation( + latest_event=latest_thread_event, + latest_edit=edit, + count=thread_count, + # If there's a thread summary it must also exist in the + # participated dictionary. + current_user_participated=participated[event_id], + ) return results diff --git a/tests/rest/client/test_relations.py b/tests/rest/client/test_relations.py index ac3243c4591b..7a759a29a0b4 100644 --- a/tests/rest/client/test_relations.py +++ b/tests/rest/client/test_relations.py @@ -547,9 +547,7 @@ def test_aggregation_must_be_annotation(self) -> None: ) self.assertEqual(400, channel.code, channel.json_body) - @unittest.override_config( - {"experimental_features": {"msc3440_enabled": True, "msc3666_enabled": True}} - ) + @unittest.override_config({"experimental_features": {"msc3666_enabled": True}}) def test_bundled_aggregations(self) -> None: """ Test that annotations, references, and threads get correctly bundled. @@ -759,7 +757,6 @@ def test_aggregation_get_event_for_thread(self) -> None: }, ) - @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_ignore_invalid_room(self) -> None: """Test that we ignore invalid relations over federation.""" # Create another room and send a message in it. @@ -1066,7 +1063,6 @@ def test_edit_reply(self) -> None: {"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict ) - @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_edit_thread(self) -> None: """Test that editing a thread works.""" @@ -1384,7 +1380,6 @@ def test_redact_relation_annotation(self) -> None: chunk = self._get_aggregations() self.assertEqual(chunk, [{"type": "m.reaction", "key": "a", "count": 1}]) - @unittest.override_config({"experimental_features": {"msc3440_enabled": True}}) def test_redact_relation_thread(self) -> None: """ Test that thread replies are properly handled after the thread reply redacted. From 3a20675a3ec043c860d0bdb6d735092fe623d48e Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 8 Mar 2022 13:52:56 -0500 Subject: [PATCH 08/12] Add a separate feature flag for stable implementation. --- synapse/rest/client/versions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 8aaa6efa774d..22bc606a4c69 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -100,7 +100,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]: # Adds support for jump to date endpoints (/timestamp_to_event) as per MSC3030 "org.matrix.msc3030": self.config.experimental.msc3030_enabled, # Adds support for thread relations, per MSC3440. - "org.matrix.msc3440": True, + "org.matrix.msc3440": self.config.experimental.msc3440_enabled, + "org.matrix.msc3440.stable": True, }, }, ) From 88e77023436a4fa71ab89c3e24e78c69b85db6e1 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 9 Mar 2022 07:35:32 -0500 Subject: [PATCH 09/12] Add a comment. Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/rest/client/versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 22bc606a4c69..9a65aa484360 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -101,7 +101,7 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]: "org.matrix.msc3030": self.config.experimental.msc3030_enabled, # Adds support for thread relations, per MSC3440. "org.matrix.msc3440": self.config.experimental.msc3440_enabled, - "org.matrix.msc3440.stable": True, + "org.matrix.msc3440.stable": True, # TODO: remove when "v1.3" is added above }, }, ) From c50cf2da934b43642140d3548177933b67b558a7 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 9 Mar 2022 07:35:07 -0500 Subject: [PATCH 10/12] Only include the unstable bundled aggregations when the config flag is enabled. --- synapse/events/utils.py | 6 +++++- synapse/server.py | 2 +- tests/rest/client/test_relations.py | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index d528d278bb8a..cbdd14d804fd 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -395,6 +395,9 @@ class EventClientSerializer: clients. """ + def __init__(self, hs: "HomeServer"): + self._msc3440_enabled = hs.config.experimental.msc3440_enabled + def serialize_event( self, event: Union[JsonDict, EventBase], @@ -521,7 +524,8 @@ def _inject_bundled_aggregations( "current_user_participated": thread.current_user_participated, } serialized_aggregations[RelationTypes.THREAD] = thread_summary - serialized_aggregations[RelationTypes.UNSTABLE_THREAD] = thread_summary + if self._msc3440_enabled: + serialized_aggregations[RelationTypes.UNSTABLE_THREAD] = thread_summary # Include the bundled aggregations in the event. if serialized_aggregations: diff --git a/synapse/server.py b/synapse/server.py index b5e2a319bcef..0a3571a99832 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -754,7 +754,7 @@ def get_oidc_handler(self) -> "OidcHandler": @cache_in_self def get_event_client_serializer(self) -> EventClientSerializer: - return EventClientSerializer() + return EventClientSerializer(self) @cache_in_self def get_password_policy_handler(self) -> PasswordPolicyHandler: diff --git a/tests/rest/client/test_relations.py b/tests/rest/client/test_relations.py index 7a759a29a0b4..785496bb5b7f 100644 --- a/tests/rest/client/test_relations.py +++ b/tests/rest/client/test_relations.py @@ -595,7 +595,6 @@ def assert_bundle(event_json: JsonDict) -> None: RelationTypes.ANNOTATION, RelationTypes.REFERENCE, RelationTypes.THREAD, - RelationTypes.UNSTABLE_THREAD, ), ) From 06caa496be70f8b848f9c9f698dfe8b26a17bede Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 9 Mar 2022 07:37:24 -0500 Subject: [PATCH 11/12] Clarify arguments for different sql clauses. --- synapse/storage/databases/main/relations.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py index c1a7a860e3f3..b9845e05fff4 100644 --- a/synapse/storage/databases/main/relations.py +++ b/synapse/storage/databases/main/relations.py @@ -522,12 +522,12 @@ def _get_thread_summaries_txn( txn.database_engine, "relates_to_id", event_ids ) - args.append(RelationTypes.THREAD) if self._msc3440_enabled: relations_clause = "(relation_type = ? OR relation_type = ?)" - args.append(RelationTypes.UNSTABLE_THREAD) + args.extend((RelationTypes.THREAD, RelationTypes.UNSTABLE_THREAD)) else: relations_clause = "relation_type = ?" + args.append(RelationTypes.THREAD) txn.execute(sql % (clause, relations_clause), args) latest_event_ids = {} @@ -559,12 +559,12 @@ def _get_thread_summaries_txn( txn.database_engine, "relates_to_id", latest_event_ids.keys() ) - args.append(RelationTypes.THREAD) if self._msc3440_enabled: relations_clause = "(relation_type = ? OR relation_type = ?)" - args.append(RelationTypes.UNSTABLE_THREAD) + args.extend((RelationTypes.THREAD, RelationTypes.UNSTABLE_THREAD)) else: relations_clause = "relation_type = ?" + args.append(RelationTypes.THREAD) txn.execute(sql % (clause, relations_clause), args) counts = dict(cast(List[Tuple[str, int]], txn.fetchall())) @@ -637,12 +637,12 @@ def _get_thread_summary_txn(txn: LoggingTransaction) -> Set[str]: txn.database_engine, "relates_to_id", event_ids ) - args.append(RelationTypes.THREAD) if self._msc3440_enabled: relations_clause = "(relation_type = ? OR relation_type = ?)" - args.append(RelationTypes.UNSTABLE_THREAD) + args.extend((RelationTypes.THREAD, RelationTypes.UNSTABLE_THREAD)) else: relations_clause = "relation_type = ?" + args.append(RelationTypes.THREAD) args.append(user_id) From cf62ececf1236ad7600a417d25012380b0c26b6e Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 9 Mar 2022 07:47:04 -0500 Subject: [PATCH 12/12] Lint --- synapse/events/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index cbdd14d804fd..b2a237c1e04a 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -38,6 +38,7 @@ from . import EventBase if TYPE_CHECKING: + from synapse.server import HomeServer from synapse.storage.databases.main.relations import BundledAggregations