From 31d023a8d7eb0ed229419921b12391601b816970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 18:03:20 +0200 Subject: [PATCH 01/24] Add MSC2285 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/rest/client/versions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 4582c274c7ad..284f66235c5c 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -82,6 +82,8 @@ def on_GET(self, request): "io.element.e2ee_forced.trusted_private": self.e2ee_forced_trusted_private, # Supports the busy presence state described in MSC3026. "org.matrix.msc3026.busy_presence": self.config.experimental.msc3026_enabled, + # Supports receiving hidden read receipts as per MSC2285 + "org.matrix.msc2285": True, }, }, ) From 2b28a13cba4be11940714fd6cc38ca206f20348a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Jul 2021 08:48:15 +0200 Subject: [PATCH 02/24] Implement MSC2285 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/handlers/receipts.py | 53 ++++++++++++++++++--- synapse/replication/tcp/client.py | 2 + synapse/rest/client/v2_alpha/read_marker.py | 13 ++++- synapse/rest/client/v2_alpha/receipts.py | 21 ++++++-- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 0059ad0f5698..561d3e2b6ead 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -16,7 +16,7 @@ from synapse.appservice import ApplicationService from synapse.handlers._base import BaseHandler -from synapse.types import JsonDict, ReadReceipt, get_domain_from_id +from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id if TYPE_CHECKING: from synapse.server import HomeServer @@ -137,7 +137,7 @@ async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool: return True async def received_client_receipt( - self, room_id: str, receipt_type: str, user_id: str, event_id: str + self, room_id: str, receipt_type: str, user_id: str, event_id: str, hidden: bool ) -> None: """Called when a client tells us a local user has read up to the given event_id in the room. @@ -147,14 +147,14 @@ async def received_client_receipt( receipt_type=receipt_type, user_id=user_id, event_ids=[event_id], - data={"ts": int(self.clock.time_msec())}, + data={"ts": int(self.clock.time_msec()), "hidden": hidden}, ) is_new = await self._handle_new_receipts([receipt]) if not is_new: return - if self.federation_sender: + if self.federation_sender and not hidden: await self.federation_sender.send_read_receipt(receipt) @@ -162,8 +162,48 @@ class ReceiptEventSource: def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() + def _filter_out_hidden( + self, events: List[JsonDict], user_id: str + ) -> List[JsonDict]: + visible_events = [] + + # filter out hidden receipts the user shouldn't see + for event in events: + content = event.get("content", {}) + new_event = event.copy() + new_event["content"] = {} + + for event_id in content.keys(): + event_content = content.get(event_id, {}) + m_read = event_content.get("m.read", {}) + + # If m_read is missing copy over the original event_content as there is nothing to process here + if not m_read: + new_event["content"][event_id] = event_content.copy() + continue + + new_users = {} + for rr_user_id in m_read.keys(): + user_rr = m_read[rr_user_id] + hidden = user_rr.get("hidden", False) + if not hidden or rr_user_id == user_id: + new_users[rr_user_id] = user_rr.copy() + if hidden: + new_users[rr_user_id].pop("hidden") + new_users[rr_user_id]["org.matrix.msc2285.hidden"] = True + + # Set new users unless empty + if len(new_users.keys()) > 0: + new_event["content"][event_id] = {"m.read": new_users} + + # Append new_event to visible_events unless empty + if len(new_event["content"].keys()) > 0: + visible_events.append(new_event) + + return visible_events + async def get_new_events( - self, from_key: int, room_ids: List[str], **kwargs + self, from_key: int, room_ids: List[str], user: UserID, **kwargs ) -> Tuple[List[JsonDict], int]: from_key = int(from_key) to_key = self.get_current_key() @@ -174,8 +214,9 @@ async def get_new_events( events = await self.store.get_linearized_receipts_for_rooms( room_ids, from_key=from_key, to_key=to_key ) + filtered_events = self._filter_out_hidden(events, user.to_string()) - return (events, to_key) + return (filtered_events, to_key) async def get_new_events_as( self, from_key: int, service: ApplicationService diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 62d780917500..c4ac6326f3eb 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -393,6 +393,8 @@ async def _on_new_receipts(self, rows): # we only want to send on receipts for our own users if not self._is_mine_id(receipt.user_id): continue + if receipt.data.get("hidden", False): + continue receipt_info = ReadReceipt( receipt.room_id, receipt.receipt_type, diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index 5988fa47e5e9..6e458d338e71 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -13,7 +13,9 @@ # limitations under the License. import logging +from http import HTTPStatus +from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request from ._base import client_patterns @@ -37,14 +39,23 @@ async def on_POST(self, request, room_id): await self.presence_handler.bump_presence_active_time(requester.user) body = parse_json_object_from_request(request) - read_event_id = body.get("m.read", None) + hidden = body.get("org.matrix.msc2285.hidden", False) + + if not isinstance(hidden, bool): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'hidden' must be a boolean, if given", + Codes.BAD_JSON, + ) + if read_event_id: await self.receipts_handler.received_client_receipt( room_id, "m.read", user_id=requester.user.to_string(), event_id=read_event_id, + hidden=hidden, ) read_marker_event_id = body.get("m.fully_read", None) diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index 8cf4aebdbec3..5af51601377a 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -13,9 +13,10 @@ # limitations under the License. import logging +from http import HTTPStatus -from synapse.api.errors import SynapseError -from synapse.http.servlet import RestServlet +from synapse.api.errors import Codes, SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request from ._base import client_patterns @@ -42,10 +43,24 @@ async def on_POST(self, request, room_id, receipt_type, event_id): if receipt_type != "m.read": raise SynapseError(400, "Receipt type must be 'm.read'") + body = parse_json_object_from_request(request) + hidden = body.get("org.matrix.msc2285.hidden", False) + + if not isinstance(hidden, bool): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'hidden' must be a boolean, if given", + Codes.BAD_JSON, + ) + await self.presence_handler.bump_presence_active_time(requester.user) await self.receipts_handler.received_client_receipt( - room_id, receipt_type, user_id=requester.user.to_string(), event_id=event_id + room_id, + receipt_type, + user_id=requester.user.to_string(), + event_id=event_id, + hidden=hidden, ) return 200, {} From c7ff003afd53d208df3f7d795ad833fe8d519b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Jul 2021 17:02:14 +0200 Subject: [PATCH 03/24] Test that hidden read receipts don't break unread counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- tests/rest/client/v2_alpha/test_sync.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index cdca3a3e2300..315f57384778 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -17,7 +17,7 @@ import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes, RelationTypes from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import knock, read_marker, sync +from synapse.rest.client.v2_alpha import knock, read_marker, receipts, sync from tests import unittest from tests.federation.transport.test_knocking import ( @@ -375,6 +375,7 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase): read_marker.register_servlets, room.register_servlets, sync.register_servlets, + receipts.register_servlets, ] def prepare(self, reactor, clock, hs): @@ -448,6 +449,23 @@ def test_unread_counts(self): # Check that the unread counter is back to 0. self._check_unread_count(0) + # Check that hidden read receipts don't break unread counts + res = self.helper.send(self.room_id, "hello", tok=self.tok2) + self._check_unread_count(1) + + # Send a read receipt to tell the server we've read the latest event. + body = json.dumps({"org.matrix.msc2285.hidden": True}).encode("utf8") + channel = self.make_request( + "POST", + "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]), + body, + access_token=self.tok, + ) + self.assertEqual(channel.code, 200, channel.json_body) + + # Check that the unread counter is back to 0. + self._check_unread_count(0) + # Check that room name changes increase the unread counter. self.helper.send_state( self.room_id, From 329ca23edf922b38f4486d0190c82b7e4cd5c37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Jul 2021 17:19:16 +0200 Subject: [PATCH 04/24] Test that hidden read receipts are hidden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- tests/rest/client/v2_alpha/test_sync.py | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 315f57384778..0bcdd3488e0c 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -368,6 +368,75 @@ def test_knock_room_state(self): ) +class ReadReceiptsTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + receipts.register_servlets, + room.register_servlets, + sync.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.url = "/sync?since=%s" + self.next_batch = "s0" + + # Register the first user + self.user_id = self.register_user("kermit", "monkey") + self.tok = self.login("kermit", "monkey") + + # Create the room + self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + + # Register the second user + self.user2 = self.register_user("kermit2", "monkey") + self.tok2 = self.login("kermit2", "monkey") + + # Join the second user + self.helper.join(room=self.room_id, user=self.user2, tok=self.tok2) + + def test_read_receipts(self): + # Send a message as the first user + res = self.helper.send(self.room_id, body="hello", tok=self.tok) + + # Send a read receipt to tell the server the first user's message was read + body = json.dumps({"org.matrix.msc2285.hidden": True}).encode("utf8") + channel = self.make_request( + "POST", + "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]), + body, + access_token=self.tok2, + ) + self.assertEqual(channel.code, 200) + + # Test that the first user can't see the other user's hidden read receipt + self.assertEqual(self._get_read_receipt(), None) + + def _get_read_receipt(self): + """Syncs and returns the read receipt.""" + + # Checks if event is a read receipt + def is_read_receipt(event): + return event["type"] == "m.receipt" + + # Sync + channel = self.make_request( + "GET", + self.url % self.next_batch, + access_token=self.tok, + ) + self.assertEqual(channel.code, 200) + + # Store the next batch for the next request. + self.next_batch = channel.json_body["next_batch"] + + # Return the read receipt + ephemeral_events = channel.json_body["rooms"]["join"][self.room_id][ + "ephemeral" + ]["events"] + return next(filter(is_read_receipt, ephemeral_events), None) + + class UnreadMessagesTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets, From 5ba7be79ac09a3741602c565341440270cdafbaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 09:33:54 +0200 Subject: [PATCH 05/24] Test filtering of hidden events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- tests/handlers/test_receipts.py | 290 ++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 tests/handlers/test_receipts.py diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py new file mode 100644 index 000000000000..438ee943dfc4 --- /dev/null +++ b/tests/handlers/test_receipts.py @@ -0,0 +1,290 @@ +# Copyright 2021 Šimon Brandner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import List + +from synapse.types import JsonDict + +from tests import unittest + + +class ReceiptsTestCase(unittest.HomeserverTestCase): + def prepare(self, reactor, clock, hs): + self.event_source = hs.get_event_sources().sources["receipt"] + + def test_hidden_receipt_filtering(self): + # Filters out a hidden receipt + self._filters_correctly( + [ + { + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453, + "hidden": True, + } + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [], + ) + + # Doesn't filter out our hidden read receipt + self._filters_correctly( + [ + { + "content": { + "$1435641916hfgh4394fHBLK:matrix.org": { + "m.read": { + "@me:server.org": { + "ts": 1436451550453, + "hidden": True, + }, + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$1435641916hfgh4394fHBLK:matrix.org": { + "m.read": { + "@me:server.org": { + "ts": 1436451550453, + "org.matrix.msc2285.hidden": True, + }, + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + # Filters out a hidden read receipt and doesn't touch the rest + self._filters_correctly( + [ + { + "content": { + "$1dgdgrd5641916114394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453, + "hidden": True, + }, + "@user:jki.re": { + "ts": 1436451550453, + }, + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$1dgdgrd5641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + # Filters out an event with only hidden read receipts and doesn't touch the rest + self._filters_correctly( + [ + { + "content": { + "$14356419edgd14394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453, + "hidden": True, + }, + } + }, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + # Handles missing content of m.read + self._filters_correctly( + [ + { + "content": { + "$14356419ggffg114394fHBLK:matrix.org": {"m.read": {}}, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$14356419ggffg114394fHBLK:matrix.org": {"m.read": {}}, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + # Handles an empty event + self._filters_correctly( + [ + { + "content": { + "$143564gdfg6114394fHBLK:matrix.org": {}, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$143564gdfg6114394fHBLK:matrix.org": {}, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + # Filters out an event with only hidden read receipts and doesn't touch the rest + self._filters_correctly( + [ + { + "content": { + "$14356419edgd14394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453, + "hidden": True, + }, + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + }, + { + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + }, + ], + [ + { + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + def _filters_correctly( + self, events: List[JsonDict], expected_output: List[JsonDict] + ): + """Tests that the _filter_out_hidden returns the expected output""" + filtered_events = self.event_source._filter_out_hidden(events, "@me:server.org") + self.assertEquals(filtered_events, expected_output) From d11783c9361314265b60335fe91e36deddd92831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 10:18:54 +0200 Subject: [PATCH 06/24] Changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- changelog.d/10413.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10413.feature diff --git a/changelog.d/10413.feature b/changelog.d/10413.feature new file mode 100644 index 000000000000..ee38d9798268 --- /dev/null +++ b/changelog.d/10413.feature @@ -0,0 +1 @@ +Support for [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-doc/pull/2285). From c3527518aa384addaa7b5e86a0613e00981c112b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Jul 2021 09:16:37 +0200 Subject: [PATCH 07/24] Make filter_out_hidden static MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/handlers/receipts.py | 7 +++---- tests/handlers/test_receipts.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 561d3e2b6ead..8150a63cac35 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -162,9 +162,8 @@ class ReceiptEventSource: def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() - def _filter_out_hidden( - self, events: List[JsonDict], user_id: str - ) -> List[JsonDict]: + @staticmethod + def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: visible_events = [] # filter out hidden receipts the user shouldn't see @@ -214,7 +213,7 @@ async def get_new_events( events = await self.store.get_linearized_receipts_for_rooms( room_ids, from_key=from_key, to_key=to_key ) - filtered_events = self._filter_out_hidden(events, user.to_string()) + filtered_events = ReceiptEventSource.filter_out_hidden(events, user.to_string()) return (filtered_events, to_key) diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py index 438ee943dfc4..95c2513aebd8 100644 --- a/tests/handlers/test_receipts.py +++ b/tests/handlers/test_receipts.py @@ -286,5 +286,5 @@ def _filters_correctly( self, events: List[JsonDict], expected_output: List[JsonDict] ): """Tests that the _filter_out_hidden returns the expected output""" - filtered_events = self.event_source._filter_out_hidden(events, "@me:server.org") + filtered_events = self.event_source.filter_out_hidden(events, "@me:server.org") self.assertEquals(filtered_events, expected_output) From 1dcccd34d9b0a1ecfeca746ce41b5247b52de629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Jul 2021 10:51:09 +0200 Subject: [PATCH 08/24] Handling of hidden read receipts for initial sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/handlers/initial_sync.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 76242865ae28..1e0e51db80b4 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -21,6 +21,7 @@ from synapse.api.errors import SynapseError from synapse.events.validator import EventValidator from synapse.handlers.presence import format_user_presence_state +from synapse.handlers.receipts import ReceiptEventSource from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.storage.roommember import RoomsForUser from synapse.streams.config import PaginationConfig @@ -126,6 +127,7 @@ async def _snapshot_all_rooms( joined_rooms, to_key=int(now_token.receipt_key), ) + receipt = ReceiptEventSource.filter_out_hidden(receipt, user_id) tags_by_room = await self.store.get_tags_for_user(user_id) @@ -422,8 +424,8 @@ async def get_receipts(): room_id, to_key=now_token.receipt_key ) if not receipts: - receipts = [] - return receipts + return [] + return ReceiptEventSource.filter_out_hidden(receipts, user_id) presence, receipts, (messages, token) = await make_deferred_yieldable( defer.gatherResults( From 59763c4d0c9b812e834243e71db05d2ae1ede5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Jul 2021 11:26:41 +0200 Subject: [PATCH 09/24] Correctly handle hidden=True MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/handlers/receipts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 8150a63cac35..215e876eee81 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -187,9 +187,10 @@ def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: hidden = user_rr.get("hidden", False) if not hidden or rr_user_id == user_id: new_users[rr_user_id] = user_rr.copy() - if hidden: + # If hidden has a value replace hidden with the correct prefixed key + if user_rr.get("hidden", None) is not None: new_users[rr_user_id].pop("hidden") - new_users[rr_user_id]["org.matrix.msc2285.hidden"] = True + new_users[rr_user_id]["org.matrix.msc2285.hidden"] = hidden # Set new users unless empty if len(new_users.keys()) > 0: From 39f830caec27fe82ac0c6587ed023fc0fd4d947b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 15:49:00 +0200 Subject: [PATCH 10/24] _filters_correctly -> _test_filters_hidden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- tests/handlers/test_receipts.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py index 95c2513aebd8..d3b7ced1962c 100644 --- a/tests/handlers/test_receipts.py +++ b/tests/handlers/test_receipts.py @@ -26,7 +26,7 @@ def prepare(self, reactor, clock, hs): def test_hidden_receipt_filtering(self): # Filters out a hidden receipt - self._filters_correctly( + self._test_filters_hidden( [ { "content": { @@ -47,7 +47,7 @@ def test_hidden_receipt_filtering(self): ) # Doesn't filter out our hidden read receipt - self._filters_correctly( + self._test_filters_hidden( [ { "content": { @@ -83,7 +83,7 @@ def test_hidden_receipt_filtering(self): ) # Filters out a hidden read receipt and doesn't touch the rest - self._filters_correctly( + self._test_filters_hidden( [ { "content": { @@ -121,7 +121,7 @@ def test_hidden_receipt_filtering(self): ) # Filters out an event with only hidden read receipts and doesn't touch the rest - self._filters_correctly( + self._test_filters_hidden( [ { "content": { @@ -163,7 +163,7 @@ def test_hidden_receipt_filtering(self): ) # Handles missing content of m.read - self._filters_correctly( + self._test_filters_hidden( [ { "content": { @@ -199,7 +199,7 @@ def test_hidden_receipt_filtering(self): ) # Handles an empty event - self._filters_correctly( + self._test_filters_hidden( [ { "content": { @@ -235,7 +235,7 @@ def test_hidden_receipt_filtering(self): ) # Filters out an event with only hidden read receipts and doesn't touch the rest - self._filters_correctly( + self._test_filters_hidden( [ { "content": { @@ -282,7 +282,7 @@ def test_hidden_receipt_filtering(self): ], ) - def _filters_correctly( + def _test_filters_hidden( self, events: List[JsonDict], expected_output: List[JsonDict] ): """Tests that the _filter_out_hidden returns the expected output""" From b8ece44cb2dd751b85c80933cae061e045ea0181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 16:10:32 +0200 Subject: [PATCH 11/24] Add an MSC2285_HIDDEN const MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/api/constants.py | 4 ++++ synapse/handlers/receipts.py | 5 ++++- synapse/rest/client/v2_alpha/read_marker.py | 3 ++- synapse/rest/client/v2_alpha/receipts.py | 3 ++- tests/handlers/test_receipts.py | 3 ++- tests/rest/client/v2_alpha/test_sync.py | 11 ++++++++--- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 8363c2bb0f5f..a0a42273efae 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -222,3 +222,7 @@ class HistoryVisibility: JOINED = "joined" SHARED = "shared" WORLD_READABLE = "world_readable" + + +class ReadReceiptEventFields: + MSC2285_HIDDEN = "org.matrix.msc2285.hidden" diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 215e876eee81..c125fd560102 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -14,6 +14,7 @@ import logging from typing import TYPE_CHECKING, List, Optional, Tuple +from synapse.api.constants import ReadReceiptEventFields from synapse.appservice import ApplicationService from synapse.handlers._base import BaseHandler from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id @@ -190,7 +191,9 @@ def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: # If hidden has a value replace hidden with the correct prefixed key if user_rr.get("hidden", None) is not None: new_users[rr_user_id].pop("hidden") - new_users[rr_user_id]["org.matrix.msc2285.hidden"] = hidden + new_users[rr_user_id][ + ReadReceiptEventFields.MSC2285_HIDDEN + ] = hidden # Set new users unless empty if len(new_users.keys()) > 0: diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index 6e458d338e71..79787bec33bb 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -15,6 +15,7 @@ import logging from http import HTTPStatus +from synapse.api.constants import ReadReceiptEventFields from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request @@ -40,7 +41,7 @@ async def on_POST(self, request, room_id): body = parse_json_object_from_request(request) read_event_id = body.get("m.read", None) - hidden = body.get("org.matrix.msc2285.hidden", False) + hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) if not isinstance(hidden, bool): raise SynapseError( diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index 5af51601377a..a8a135ac105f 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -15,6 +15,7 @@ import logging from http import HTTPStatus +from synapse.api.constants import ReadReceiptEventFields from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request @@ -44,7 +45,7 @@ async def on_POST(self, request, room_id, receipt_type, event_id): raise SynapseError(400, "Receipt type must be 'm.read'") body = parse_json_object_from_request(request) - hidden = body.get("org.matrix.msc2285.hidden", False) + hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) if not isinstance(hidden, bool): raise SynapseError( diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py index d3b7ced1962c..fb9086534a53 100644 --- a/tests/handlers/test_receipts.py +++ b/tests/handlers/test_receipts.py @@ -15,6 +15,7 @@ from typing import List +from synapse.api.constants import ReadReceiptEventFields from synapse.types import JsonDict from tests import unittest @@ -71,7 +72,7 @@ def test_hidden_receipt_filtering(self): "m.read": { "@me:server.org": { "ts": 1436451550453, - "org.matrix.msc2285.hidden": True, + ReadReceiptEventFields.MSC2285_HIDDEN: True, }, } } diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 0bcdd3488e0c..38acd2f0e163 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -15,7 +15,12 @@ import json import synapse.rest.admin -from synapse.api.constants import EventContentFields, EventTypes, RelationTypes +from synapse.api.constants import ( + EventContentFields, + EventTypes, + ReadReceiptEventFields, + RelationTypes, +) from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import knock, read_marker, receipts, sync @@ -400,7 +405,7 @@ def test_read_receipts(self): res = self.helper.send(self.room_id, body="hello", tok=self.tok) # Send a read receipt to tell the server the first user's message was read - body = json.dumps({"org.matrix.msc2285.hidden": True}).encode("utf8") + body = json.dumps({ReadReceiptEventFields.MSC2285_HIDDEN: True}).encode("utf8") channel = self.make_request( "POST", "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]), @@ -523,7 +528,7 @@ def test_unread_counts(self): self._check_unread_count(1) # Send a read receipt to tell the server we've read the latest event. - body = json.dumps({"org.matrix.msc2285.hidden": True}).encode("utf8") + body = json.dumps({ReadReceiptEventFields.MSC2285_HIDDEN: True}).encode("utf8") channel = self.make_request( "POST", "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]), From b90949b4d9e1afed47d4803ea37bdc383fa3069f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 16:14:43 +0200 Subject: [PATCH 12/24] Use correct param name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/rest/client/v2_alpha/read_marker.py | 3 ++- synapse/rest/client/v2_alpha/receipts.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index 79787bec33bb..38b256a37ddb 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -46,7 +46,8 @@ async def on_POST(self, request, room_id): if not isinstance(hidden, bool): raise SynapseError( HTTPStatus.BAD_REQUEST, - "Param 'hidden' must be a boolean, if given", + "Param %s must be a boolean, if given" + % ReadReceiptEventFields.MSC2285_HIDDEN, Codes.BAD_JSON, ) diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index a8a135ac105f..65d1a1da3724 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -50,7 +50,8 @@ async def on_POST(self, request, room_id, receipt_type, event_id): if not isinstance(hidden, bool): raise SynapseError( HTTPStatus.BAD_REQUEST, - "Param 'hidden' must be a boolean, if given", + "Param %s must be a boolean, if given" + % ReadReceiptEventFields.MSC2285_HIDDEN, Codes.BAD_JSON, ) From 93c87aa2133aab2e929968f5af9bcdb04e9a5602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 16:15:56 +0200 Subject: [PATCH 13/24] Use 400 for consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/rest/client/v2_alpha/read_marker.py | 2 +- synapse/rest/client/v2_alpha/receipts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index 38b256a37ddb..6782a1bd9418 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -45,7 +45,7 @@ async def on_POST(self, request, room_id): if not isinstance(hidden, bool): raise SynapseError( - HTTPStatus.BAD_REQUEST, + 400, "Param %s must be a boolean, if given" % ReadReceiptEventFields.MSC2285_HIDDEN, Codes.BAD_JSON, diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index 65d1a1da3724..0f44fdd35538 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -49,7 +49,7 @@ async def on_POST(self, request, room_id, receipt_type, event_id): if not isinstance(hidden, bool): raise SynapseError( - HTTPStatus.BAD_REQUEST, + 400, "Param %s must be a boolean, if given" % ReadReceiptEventFields.MSC2285_HIDDEN, Codes.BAD_JSON, From 77cd0d81da8fd785aa9a56ff1b40d87f42616800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 16:19:55 +0200 Subject: [PATCH 14/24] Add a comment about not using prefixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- tests/handlers/test_receipts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py index fb9086534a53..cbe193c6fe1c 100644 --- a/tests/handlers/test_receipts.py +++ b/tests/handlers/test_receipts.py @@ -26,6 +26,12 @@ def prepare(self, reactor, clock, hs): self.event_source = hs.get_event_sources().sources["receipt"] def test_hidden_receipt_filtering(self): + """ + In the first param of _test_filters_hidden we use "hidden" instead of + ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking + the data from the database which doesn't use the prefix + """ + # Filters out a hidden receipt self._test_filters_hidden( [ From 15eca460d99700314dfb7f7ca89c5ebf5fa6608d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 16:30:00 +0200 Subject: [PATCH 15/24] Split out into seperate methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I am sorry for the long names, really Signed-off-by: Šimon Brandner --- tests/handlers/test_receipts.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py index cbe193c6fe1c..621e229471bb 100644 --- a/tests/handlers/test_receipts.py +++ b/tests/handlers/test_receipts.py @@ -25,14 +25,7 @@ class ReceiptsTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): self.event_source = hs.get_event_sources().sources["receipt"] - def test_hidden_receipt_filtering(self): - """ - In the first param of _test_filters_hidden we use "hidden" instead of - ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking - the data from the database which doesn't use the prefix - """ - - # Filters out a hidden receipt + def test_filters_out_hidden_receipt(self): self._test_filters_hidden( [ { @@ -53,7 +46,7 @@ def test_hidden_receipt_filtering(self): [], ) - # Doesn't filter out our hidden read receipt + def test_does_not_filter_out_our_hidden_receipt(self): self._test_filters_hidden( [ { @@ -89,7 +82,7 @@ def test_hidden_receipt_filtering(self): ], ) - # Filters out a hidden read receipt and doesn't touch the rest + def test_filters_out_hidden_receipt_and_ignores_rest(self): self._test_filters_hidden( [ { @@ -127,7 +120,7 @@ def test_hidden_receipt_filtering(self): ], ) - # Filters out an event with only hidden read receipts and doesn't touch the rest + def test_filters_out_event_with_only_hidden_receipts_and_ignores_the_rest(self): self._test_filters_hidden( [ { @@ -169,7 +162,7 @@ def test_hidden_receipt_filtering(self): ], ) - # Handles missing content of m.read + def test_handles_missing_content_of_m_read(self): self._test_filters_hidden( [ { @@ -205,7 +198,7 @@ def test_hidden_receipt_filtering(self): ], ) - # Handles an empty event + def test_handles_empty_event(self): self._test_filters_hidden( [ { @@ -241,7 +234,7 @@ def test_hidden_receipt_filtering(self): ], ) - # Filters out an event with only hidden read receipts and doesn't touch the rest + def test_filters_out_receipt_event_with_only_hidden_receipt_and_ignores_rest(self): self._test_filters_hidden( [ { @@ -289,6 +282,12 @@ def test_hidden_receipt_filtering(self): ], ) + """ + In the first param of _test_filters_hidden we use "hidden" instead of + ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking + the data from the database which doesn't use the prefix + """ + def _test_filters_hidden( self, events: List[JsonDict], expected_output: List[JsonDict] ): From dc4e8acbc0da1b7c07e465523367f0ec9c5cbb9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 16:41:22 +0200 Subject: [PATCH 16/24] Default to hidden = None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/handlers/receipts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index c125fd560102..ca7fe0ba0943 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -185,11 +185,11 @@ def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: new_users = {} for rr_user_id in m_read.keys(): user_rr = m_read[rr_user_id] - hidden = user_rr.get("hidden", False) - if not hidden or rr_user_id == user_id: + hidden = user_rr.get("hidden", None) + if hidden != True or rr_user_id == user_id: new_users[rr_user_id] = user_rr.copy() # If hidden has a value replace hidden with the correct prefixed key - if user_rr.get("hidden", None) is not None: + if hidden is not None: new_users[rr_user_id].pop("hidden") new_users[rr_user_id][ ReadReceiptEventFields.MSC2285_HIDDEN From 397ffea47edcd71404f80658f5324a946fed97a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 16:42:23 +0200 Subject: [PATCH 17/24] Iterate over both keys and values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/handlers/receipts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index ca7fe0ba0943..c9f4c2a2d420 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -183,8 +183,7 @@ def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: continue new_users = {} - for rr_user_id in m_read.keys(): - user_rr = m_read[rr_user_id] + for rr_user_id, user_rr in m_read.items(): hidden = user_rr.get("hidden", None) if hidden != True or rr_user_id == user_id: new_users[rr_user_id] = user_rr.copy() From fa90c662974e97c937a056d202cf7369ab890066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 16:46:18 +0200 Subject: [PATCH 18/24] test_read_receipts -> test_hidden_read_receipts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- tests/rest/client/v2_alpha/test_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 38acd2f0e163..6a6778632f15 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -400,7 +400,7 @@ def prepare(self, reactor, clock, hs): # Join the second user self.helper.join(room=self.room_id, user=self.user2, tok=self.tok2) - def test_read_receipts(self): + def test_hidden_read_receipts(self): # Send a message as the first user res = self.helper.send(self.room_id, body="hello", tok=self.tok) From d0d4dc6a655fc59f313b12516b13743b88a76937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 17:16:37 +0200 Subject: [PATCH 19/24] Hide MSC2285 behind a feature flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/config/experimental.py | 3 +++ synapse/handlers/initial_sync.py | 7 +++++-- synapse/handlers/receipts.py | 11 ++++++++--- synapse/replication/tcp/client.py | 5 ++++- synapse/rest/client/versions.py | 2 +- tests/rest/client/v2_alpha/test_sync.py | 1 + 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 7fb1f7021f5e..3565841cfed5 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -32,3 +32,6 @@ def read_config(self, config: JsonDict, **kwargs): # MSC2716 (backfill existing history) self.msc2716_enabled = experimental.get("msc2716_enabled", False) # type: bool + + # MSC2285 (hidden read receipts) + self.msc2285_enabled = experimental.get("msc2285_enabled", False) # type: bool diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 1e0e51db80b4..b26df1986ee5 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -127,7 +127,8 @@ async def _snapshot_all_rooms( joined_rooms, to_key=int(now_token.receipt_key), ) - receipt = ReceiptEventSource.filter_out_hidden(receipt, user_id) + if self.hs.config.experimental.msc2285_enabled: + receipt = ReceiptEventSource.filter_out_hidden(receipt, user_id) tags_by_room = await self.store.get_tags_for_user(user_id) @@ -425,7 +426,9 @@ async def get_receipts(): ) if not receipts: return [] - return ReceiptEventSource.filter_out_hidden(receipts, user_id) + if self.hs.config.experimental.msc2285_enabled: + receipts = ReceiptEventSource.filter_out_hidden(receipts, user_id) + return receipts presence, receipts, (messages, token) = await make_deferred_yieldable( defer.gatherResults( diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index c9f4c2a2d420..2ef69ed098a0 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -155,13 +155,16 @@ async def received_client_receipt( if not is_new: return - if self.federation_sender and not hidden: + if self.federation_sender and ( + not hidden or not self.hs.config.experimental.msc2285_enabled + ): await self.federation_sender.send_read_receipt(receipt) class ReceiptEventSource: def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() + self.config = hs.config @staticmethod def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: @@ -216,9 +219,11 @@ async def get_new_events( events = await self.store.get_linearized_receipts_for_rooms( room_ids, from_key=from_key, to_key=to_key ) - filtered_events = ReceiptEventSource.filter_out_hidden(events, user.to_string()) - return (filtered_events, to_key) + if self.config.experimental.msc2285_enabled: + events = ReceiptEventSource.filter_out_hidden(events, user.to_string()) + + return (events, to_key) async def get_new_events_as( self, from_key: int, service: ApplicationService diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index c4ac6326f3eb..ddb968123b9c 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -393,7 +393,10 @@ async def _on_new_receipts(self, rows): # we only want to send on receipts for our own users if not self._is_mine_id(receipt.user_id): continue - if receipt.data.get("hidden", False): + if ( + receipt.data.get("hidden", False) + and self._hs.config.experimental.msc2285_enabled + ): continue receipt_info = ReadReceipt( receipt.room_id, diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 284f66235c5c..fa2e4e9cba48 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -83,7 +83,7 @@ def on_GET(self, request): # Supports the busy presence state described in MSC3026. "org.matrix.msc3026.busy_presence": self.config.experimental.msc3026_enabled, # Supports receiving hidden read receipts as per MSC2285 - "org.matrix.msc2285": True, + "org.matrix.msc2285": self.config.experimental.msc2285_enabled, }, }, ) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 6a6778632f15..f6ae9ae18110 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -400,6 +400,7 @@ def prepare(self, reactor, clock, hs): # Join the second user self.helper.join(room=self.room_id, user=self.user2, tok=self.tok2) + @override_config({"experimental_features": {"msc2285_enabled": True}}) def test_hidden_read_receipts(self): # Send a message as the first user res = self.helper.send(self.room_id, body="hello", tok=self.tok) From 7743ab8e75e509f1018c04f0b5a0f86e3d2effa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 17:18:21 +0200 Subject: [PATCH 20/24] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- synapse/handlers/receipts.py | 2 +- synapse/rest/client/v2_alpha/read_marker.py | 1 - synapse/rest/client/v2_alpha/receipts.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 2ef69ed098a0..7259bd19626a 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -188,7 +188,7 @@ def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: new_users = {} for rr_user_id, user_rr in m_read.items(): hidden = user_rr.get("hidden", None) - if hidden != True or rr_user_id == user_id: + if hidden is not True or rr_user_id == user_id: new_users[rr_user_id] = user_rr.copy() # If hidden has a value replace hidden with the correct prefixed key if hidden is not None: diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index 6782a1bd9418..027f8b81fa93 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -13,7 +13,6 @@ # limitations under the License. import logging -from http import HTTPStatus from synapse.api.constants import ReadReceiptEventFields from synapse.api.errors import Codes, SynapseError diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index 0f44fdd35538..4b98979b477d 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -13,7 +13,6 @@ # limitations under the License. import logging -from http import HTTPStatus from synapse.api.constants import ReadReceiptEventFields from synapse.api.errors import Codes, SynapseError From 49dfd1d8cbc00cb3aee112d01510f01c65151991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Jul 2021 18:32:59 +0200 Subject: [PATCH 21/24] Move comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- tests/handlers/test_receipts.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py index 621e229471bb..64ea00956d77 100644 --- a/tests/handlers/test_receipts.py +++ b/tests/handlers/test_receipts.py @@ -20,6 +20,10 @@ from tests import unittest +# In the first param of _test_filters_hidden we use "hidden" instead of +# ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking +# the data from the database which doesn't use the prefix + class ReceiptsTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): @@ -282,12 +286,6 @@ def test_filters_out_receipt_event_with_only_hidden_receipt_and_ignores_rest(sel ], ) - """ - In the first param of _test_filters_hidden we use "hidden" instead of - ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking - the data from the database which doesn't use the prefix - """ - def _test_filters_hidden( self, events: List[JsonDict], expected_output: List[JsonDict] ): From 944ee1768b5e11efc52d553ce410f3f4d8d9fb90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Jul 2021 18:33:45 +0200 Subject: [PATCH 22/24] Make if statement more readable Co-authored-by: Brendan Abolivier --- synapse/handlers/receipts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index ea5cd34a2ccf..b9085bbccb39 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -155,8 +155,8 @@ async def received_client_receipt( if not is_new: return - if self.federation_sender and ( - not hidden or not self.hs.config.experimental.msc2285_enabled + if self.federation_sender and not ( + self.hs.config.experimental.msc2285_enabled and hidden ): await self.federation_sender.send_read_receipt(receipt) From a02cf5721ff5f2d86a7594943b3b155fcd1818a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Jul 2021 18:41:16 +0200 Subject: [PATCH 23/24] Move comment to an even better place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- tests/handlers/test_receipts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py index 64ea00956d77..93a9a084b24b 100644 --- a/tests/handlers/test_receipts.py +++ b/tests/handlers/test_receipts.py @@ -20,15 +20,15 @@ from tests import unittest -# In the first param of _test_filters_hidden we use "hidden" instead of -# ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking -# the data from the database which doesn't use the prefix - class ReceiptsTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): self.event_source = hs.get_event_sources().sources["receipt"] + # In the first param of _test_filters_hidden we use "hidden" instead of + # ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking + # the data from the database which doesn't use the prefix + def test_filters_out_hidden_receipt(self): self._test_filters_hidden( [ From f6ba3ac6853edc40d333e084d4c167ccba1247d6 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 27 Jul 2021 18:46:30 +0200 Subject: [PATCH 24/24] Update changelog.d/10413.feature --- changelog.d/10413.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/10413.feature b/changelog.d/10413.feature index ee38d9798268..3964db7e0eae 100644 --- a/changelog.d/10413.feature +++ b/changelog.d/10413.feature @@ -1 +1 @@ -Support for [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-doc/pull/2285). +Support for [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-doc/pull/2285). Contributed by @SimonBrandner.