diff --git a/synapse/handlers/relations.py b/synapse/handlers/relations.py index b1158ee77..756919de6 100644 --- a/synapse/handlers/relations.py +++ b/synapse/handlers/relations.py @@ -434,9 +434,15 @@ async def _get_threads_for_events( return results - @trace + # Beep beep: bundled aggregations aren't used in Beeper clients and are thus a wasted calculation async def get_bundled_aggregations( self, events: Iterable[EventBase], user_id: str + ) -> Dict[str, BundledAggregations]: + return {} + + @trace + async def _orig_get_bundled_aggregations( + self, events: Iterable[EventBase], user_id: str ) -> Dict[str, BundledAggregations]: """Generate bundled aggregations for events. diff --git a/tests/rest/client/test_relations.py b/tests/rest/client/test_relations.py index 21fb86367..5b2d24b5b 100644 --- a/tests/rest/client/test_relations.py +++ b/tests/rest/client/test_relations.py @@ -1072,508 +1072,6 @@ def test_recursive_relations_with_filter(self) -> None: self.assertEqual(event_ids, [annotation_1]) -class BundledAggregationsTestCase(BaseRelationsTestCase): - """ - See RelationsTestCase.test_edit for a similar test for edits. - - Note that this doesn't test against /relations since only thread relations - get bundled via that API. See test_aggregation_get_event_for_thread. - """ - - def _test_bundled_aggregations( - self, - relation_type: str, - assertion_callable: Callable[[JsonDict], None], - expected_db_txn_for_event: int, - access_token: Optional[str] = None, - ) -> None: - """ - Makes requests to various endpoints which should include bundled aggregations - and then calls an assertion function on the bundled aggregations. - - Args: - relation_type: The field to search for in the `m.relations` field in unsigned. - assertion_callable: Called with the contents of unsigned["m.relations"][relation_type] - for relation-specific assertions. - expected_db_txn_for_event: The number of database transactions which - are expected for a call to /event/. - access_token: The access token to user, defaults to self.user_token. - """ - access_token = access_token or self.user_token - - def assert_bundle(event_json: JsonDict) -> None: - """Assert the expected values of the bundled aggregations.""" - relations_dict = event_json["unsigned"].get("m.relations") - - # Ensure the fields are as expected. - self.assertCountEqual(relations_dict.keys(), (relation_type,)) - assertion_callable(relations_dict[relation_type]) - - # Request the event directly. - channel = self.make_request( - "GET", - f"/rooms/{self.room}/event/{self.parent_id}", - access_token=access_token, - ) - self.assertEqual(200, channel.code, channel.json_body) - assert_bundle(channel.json_body) - assert channel.resource_usage is not None - self.assertEqual(channel.resource_usage.db_txn_count, expected_db_txn_for_event) - - # Request the room messages. - channel = self.make_request( - "GET", - f"/rooms/{self.room}/messages?dir=b", - access_token=access_token, - ) - self.assertEqual(200, channel.code, channel.json_body) - assert_bundle(self._find_event_in_chunk(channel.json_body["chunk"])) - - # Request the room context. - channel = self.make_request( - "GET", - f"/rooms/{self.room}/context/{self.parent_id}", - access_token=access_token, - ) - self.assertEqual(200, channel.code, channel.json_body) - assert_bundle(channel.json_body["event"]) - - # Request sync. - filter = urllib.parse.quote_plus(b'{"room": {"timeline": {"limit": 4}}}') - channel = self.make_request( - "GET", f"/sync?filter={filter}", access_token=access_token - ) - self.assertEqual(200, channel.code, channel.json_body) - room_timeline = channel.json_body["rooms"]["join"][self.room]["timeline"] - self.assertTrue(room_timeline["limited"]) - assert_bundle(self._find_event_in_chunk(room_timeline["events"])) - - # Request search. - channel = self.make_request( - "POST", - "/search", - # Search term matches the parent message. - content={"search_categories": {"room_events": {"search_term": "Hi"}}}, - access_token=access_token, - ) - self.assertEqual(200, channel.code, channel.json_body) - chunk = [ - result["result"] - for result in channel.json_body["search_categories"]["room_events"][ - "results" - ] - ] - assert_bundle(self._find_event_in_chunk(chunk)) - - def test_reference(self) -> None: - """ - Test that references get correctly bundled. - """ - channel = self._send_relation(RelationTypes.REFERENCE, "m.room.test") - reply_1 = channel.json_body["event_id"] - - channel = self._send_relation(RelationTypes.REFERENCE, "m.room.test") - reply_2 = channel.json_body["event_id"] - - def assert_annotations(bundled_aggregations: JsonDict) -> None: - self.assertEqual( - {"chunk": [{"event_id": reply_1}, {"event_id": reply_2}]}, - bundled_aggregations, - ) - - self._test_bundled_aggregations(RelationTypes.REFERENCE, assert_annotations, 7) - - def test_thread(self) -> None: - """ - Test that threads get correctly bundled. - """ - # The root message is from "user", send replies as "user2". - self._send_relation( - RelationTypes.THREAD, "m.room.test", access_token=self.user2_token - ) - channel = self._send_relation( - RelationTypes.THREAD, "m.room.test", access_token=self.user2_token - ) - thread_2 = channel.json_body["event_id"] - - # This needs two assertion functions which are identical except for whether - # the current_user_participated flag is True, create a factory for the - # two versions. - def _gen_assert(participated: bool) -> Callable[[JsonDict], None]: - def assert_thread(bundled_aggregations: JsonDict) -> None: - self.assertEqual(2, bundled_aggregations.get("count")) - self.assertEqual( - participated, bundled_aggregations.get("current_user_participated") - ) - # The latest thread event has some fields that don't matter. - self.assertIn("latest_event", bundled_aggregations) - self.assert_dict( - { - "content": { - "m.relates_to": { - "event_id": self.parent_id, - "rel_type": RelationTypes.THREAD, - } - }, - "event_id": thread_2, - "sender": self.user2_id, - "type": "m.room.test", - }, - bundled_aggregations["latest_event"], - ) - - return assert_thread - - # The "user" sent the root event and is making queries for the bundled - # aggregations: they have participated. - self._test_bundled_aggregations(RelationTypes.THREAD, _gen_assert(True), 7) - # The "user2" sent replies in the thread and is making queries for the - # bundled aggregations: they have participated. - # - # Note that this re-uses some cached values, so the total number of - # queries is much smaller. - self._test_bundled_aggregations( - RelationTypes.THREAD, _gen_assert(True), 4, access_token=self.user2_token - ) - - # A user with no interactions with the thread: they have not participated. - user3_id, user3_token = self._create_user("charlie") - self.helper.join(self.room, user=user3_id, tok=user3_token) - self._test_bundled_aggregations( - RelationTypes.THREAD, _gen_assert(False), 4, access_token=user3_token - ) - - def test_thread_with_bundled_aggregations_for_latest(self) -> None: - """ - Bundled aggregations should get applied to the latest thread event. - """ - self._send_relation(RelationTypes.THREAD, "m.room.test") - channel = self._send_relation(RelationTypes.THREAD, "m.room.test") - thread_2 = channel.json_body["event_id"] - - channel = self._send_relation( - RelationTypes.REFERENCE, "org.matrix.test", parent_id=thread_2 - ) - reference_event_id = channel.json_body["event_id"] - - def assert_thread(bundled_aggregations: JsonDict) -> None: - self.assertEqual(2, bundled_aggregations.get("count")) - self.assertTrue(bundled_aggregations.get("current_user_participated")) - # The latest thread event has some fields that don't matter. - self.assertIn("latest_event", bundled_aggregations) - self.assert_dict( - { - "content": { - "m.relates_to": { - "event_id": self.parent_id, - "rel_type": RelationTypes.THREAD, - } - }, - "event_id": thread_2, - "sender": self.user_id, - "type": "m.room.test", - }, - bundled_aggregations["latest_event"], - ) - # Check the unsigned field on the latest event. - self.assert_dict( - { - "m.relations": { - RelationTypes.REFERENCE: { - "chunk": [{"event_id": reference_event_id}] - }, - } - }, - bundled_aggregations["latest_event"].get("unsigned"), - ) - - self._test_bundled_aggregations(RelationTypes.THREAD, assert_thread, 7) - - def test_nested_thread(self) -> None: - """ - Ensure that a nested thread gets ignored by bundled aggregations, as - those are forbidden. - """ - - # Start a thread. - channel = self._send_relation(RelationTypes.THREAD, "m.room.test") - reply_event_id = channel.json_body["event_id"] - - # Disable the validation to pretend this came over federation, since it is - # not an event the Client-Server API will allow.. - with patch( - "synapse.handlers.message.EventCreationHandler._validate_event_relation", - new_callable=AsyncMock, - return_value=None, - ): - # Create a sub-thread off the thread, which is not allowed. - self._send_relation( - RelationTypes.THREAD, "m.room.test", parent_id=reply_event_id - ) - - # Fetch the thread root, to get the bundled aggregation for the thread. - relations_from_event = self._get_bundled_aggregations() - - # Ensure that requesting the room messages also does not return the sub-thread. - channel = self.make_request( - "GET", - f"/rooms/{self.room}/messages?dir=b", - access_token=self.user_token, - ) - self.assertEqual(200, channel.code, channel.json_body) - event = self._find_event_in_chunk(channel.json_body["chunk"]) - relations_from_messages = event["unsigned"]["m.relations"] - - # Check the bundled aggregations from each point. - for aggregations, desc in ( - (relations_from_event, "/event"), - (relations_from_messages, "/messages"), - ): - # The latest event should have bundled aggregations. - self.assertIn(RelationTypes.THREAD, aggregations, desc) - thread_summary = aggregations[RelationTypes.THREAD] - self.assertIn("latest_event", thread_summary, desc) - self.assertEqual( - thread_summary["latest_event"]["event_id"], reply_event_id, desc - ) - - # The latest event should not have any bundled aggregations (since the - # only relation to it is another thread, which is invalid). - self.assertNotIn( - "m.relations", thread_summary["latest_event"]["unsigned"], desc - ) - - def test_thread_edit_latest_event(self) -> None: - """Test that editing the latest event in a thread works.""" - - # Create a thread and edit the last event. - channel = self._send_relation( - RelationTypes.THREAD, - "m.room.message", - content={"msgtype": "m.text", "body": "A threaded reply!"}, - ) - threaded_event_id = channel.json_body["event_id"] - - new_body = {"msgtype": "m.text", "body": "I've been edited!"} - channel = self._send_relation( - RelationTypes.REPLACE, - "m.room.message", - content={"msgtype": "m.text", "body": "foo", "m.new_content": new_body}, - parent_id=threaded_event_id, - ) - edit_event_id = channel.json_body["event_id"] - - # Fetch the thread root, to get the bundled aggregation for the thread. - relations_dict = self._get_bundled_aggregations() - - # We expect that the edit message appears in the thread summary in the - # unsigned relations section. - self.assertIn(RelationTypes.THREAD, relations_dict) - - thread_summary = relations_dict[RelationTypes.THREAD] - self.assertIn("latest_event", thread_summary) - latest_event_in_thread = thread_summary["latest_event"] - # The latest event in the thread should have the edit appear under the - # bundled aggregations. - self.assertLessEqual( - {"event_id": edit_event_id, "sender": "@alice:test"}.items(), - latest_event_in_thread["unsigned"]["m.relations"][ - RelationTypes.REPLACE - ].items(), - ) - - def test_aggregation_get_event_for_annotation(self) -> None: - """Test that annotations do not get bundled aggregations included - when directly requested. - """ - channel = self._send_relation(RelationTypes.ANNOTATION, "m.reaction", "a") - annotation_id = channel.json_body["event_id"] - - # Annotate the annotation. - self._send_relation( - RelationTypes.ANNOTATION, "m.reaction", "a", parent_id=annotation_id - ) - - channel = self.make_request( - "GET", - f"/rooms/{self.room}/event/{annotation_id}", - access_token=self.user_token, - ) - self.assertEqual(200, channel.code, channel.json_body) - self.assertIsNone(channel.json_body["unsigned"].get("m.relations")) - - def test_aggregation_get_event_for_thread(self) -> None: - """Test that threads get bundled aggregations included when directly requested.""" - channel = self._send_relation(RelationTypes.THREAD, "m.room.test") - thread_id = channel.json_body["event_id"] - - # Make a reference to the thread. - channel = self._send_relation( - RelationTypes.REFERENCE, "org.matrix.test", parent_id=thread_id - ) - reference_event_id = channel.json_body["event_id"] - - channel = self.make_request( - "GET", - f"/rooms/{self.room}/event/{thread_id}", - access_token=self.user_token, - ) - self.assertEqual(200, channel.code, channel.json_body) - self.assertEqual( - channel.json_body["unsigned"].get("m.relations"), - { - RelationTypes.REFERENCE: {"chunk": [{"event_id": reference_event_id}]}, - }, - ) - - # It should also be included when the entire thread is requested. - channel = self.make_request( - "GET", - f"/_matrix/client/v1/rooms/{self.room}/relations/{self.parent_id}?limit=1", - access_token=self.user_token, - ) - self.assertEqual(200, channel.code, channel.json_body) - self.assertEqual(len(channel.json_body["chunk"]), 1) - - thread_message = channel.json_body["chunk"][0] - self.assertEqual( - thread_message["unsigned"].get("m.relations"), - { - RelationTypes.REFERENCE: {"chunk": [{"event_id": reference_event_id}]}, - }, - ) - - def test_bundled_aggregations_with_filter(self) -> None: - """ - If "unsigned" is an omitted field (due to filtering), adding the bundled - aggregations should not break. - - Note that the spec allows for a server to return additional fields beyond - what is specified. - """ - channel = self._send_relation(RelationTypes.REFERENCE, "org.matrix.test") - reference_event_id = channel.json_body["event_id"] - - # Note that the sync filter does not include "unsigned" as a field. - filter = urllib.parse.quote_plus( - b'{"event_fields": ["content", "event_id"], "room": {"timeline": {"limit": 3}}}' - ) - channel = self.make_request( - "GET", f"/sync?filter={filter}", access_token=self.user_token - ) - self.assertEqual(200, channel.code, channel.json_body) - - # Ensure the timeline is limited, find the parent event. - room_timeline = channel.json_body["rooms"]["join"][self.room]["timeline"] - self.assertTrue(room_timeline["limited"]) - parent_event = self._find_event_in_chunk(room_timeline["events"]) - - # Ensure there's bundled aggregations on it. - self.assertIn("unsigned", parent_event) - self.assertEqual( - parent_event["unsigned"].get("m.relations"), - { - RelationTypes.REFERENCE: {"chunk": [{"event_id": reference_event_id}]}, - }, - ) - - -class RelationIgnoredUserTestCase(BaseRelationsTestCase): - """Relations sent from an ignored user should be ignored.""" - - def _test_ignored_user( - self, - relation_type: str, - allowed_event_ids: List[str], - ignored_event_ids: List[str], - ) -> Tuple[JsonDict, JsonDict]: - """ - Fetch the relations and ensure they're all there, then ignore user2, and - repeat. - - Returns: - A tuple of two JSON dictionaries, each are bundled aggregations, the - first is from before the user is ignored, and the second is after. - """ - # Get the relations. - event_ids = self._get_related_events() - self.assertCountEqual(event_ids, allowed_event_ids + ignored_event_ids) - - # And the bundled aggregations. - before_aggregations = self._get_bundled_aggregations() - self.assertIn(relation_type, before_aggregations) - - # Ignore user2 and re-do the requests. - self.get_success( - self.store.add_account_data_for_user( - self.user_id, - AccountDataTypes.IGNORED_USER_LIST, - {"ignored_users": {self.user2_id: {}}}, - ) - ) - - # Get the relations. - event_ids = self._get_related_events() - self.assertCountEqual(event_ids, allowed_event_ids) - - # And the bundled aggregations. - after_aggregations = self._get_bundled_aggregations() - self.assertIn(relation_type, after_aggregations) - - return before_aggregations[relation_type], after_aggregations[relation_type] - - def test_reference(self) -> None: - """Aggregations should exclude reference relations from ignored users""" - channel = self._send_relation(RelationTypes.REFERENCE, "m.room.test") - allowed_event_ids = [channel.json_body["event_id"]] - - channel = self._send_relation( - RelationTypes.REFERENCE, "m.room.test", access_token=self.user2_token - ) - ignored_event_ids = [channel.json_body["event_id"]] - - before_aggregations, after_aggregations = self._test_ignored_user( - RelationTypes.REFERENCE, allowed_event_ids, ignored_event_ids - ) - - self.assertCountEqual( - [e["event_id"] for e in before_aggregations["chunk"]], - allowed_event_ids + ignored_event_ids, - ) - - self.assertCountEqual( - [e["event_id"] for e in after_aggregations["chunk"]], allowed_event_ids - ) - - def test_thread(self) -> None: - """Aggregations should exclude thread releations from ignored users""" - channel = self._send_relation(RelationTypes.THREAD, "m.room.test") - allowed_event_ids = [channel.json_body["event_id"]] - - channel = self._send_relation( - RelationTypes.THREAD, "m.room.test", access_token=self.user2_token - ) - ignored_event_ids = [channel.json_body["event_id"]] - - before_aggregations, after_aggregations = self._test_ignored_user( - RelationTypes.THREAD, allowed_event_ids, ignored_event_ids - ) - - self.assertEqual(before_aggregations["count"], 2) - self.assertTrue(before_aggregations["current_user_participated"]) - # The latest thread event has some fields that don't matter. - self.assertEqual( - before_aggregations["latest_event"]["event_id"], ignored_event_ids[0] - ) - - self.assertEqual(after_aggregations["count"], 1) - self.assertTrue(after_aggregations["current_user_participated"]) - # The latest thread event has some fields that don't matter. - self.assertEqual( - after_aggregations["latest_event"]["event_id"], allowed_event_ids[0] - ) - - class RelationRedactionTestCase(BaseRelationsTestCase): """ Test the behaviour of relations when the parent or child event is redacted.