diff --git a/changelog.d/5990.feature b/changelog.d/5990.feature new file mode 100644 index 000000000000..5b20245ebd04 --- /dev/null +++ b/changelog.d/5990.feature @@ -0,0 +1 @@ +Support a way for clients to not send read receipts to other users/servers. diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index 04fbb407af56..802788b7fae5 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -262,6 +262,8 @@ 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): + return # do not send over federation receipt_info = ReadReceipt( receipt.room_id, receipt.receipt_type, diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index f991efeee396..7dcc1c3c351d 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -395,7 +395,12 @@ def get_receipts(): ) if not receipts: receipts = [] - return receipts + + return [ + r + for r in receipts + if not r.data.get("hidden", False) or r.user_id == user_id + ] presence, receipts, (messages, token) = yield make_deferred_yieldable( defer.gatherResults( diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 6854c751a60a..73bf7ef1444d 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -106,7 +106,7 @@ def _handle_new_receipts(self, receipts): return True @defer.inlineCallbacks - def received_client_receipt(self, room_id, receipt_type, user_id, event_id): + def received_client_receipt(self, room_id, receipt_type, user_id, event_id, hidden): """Called when a client tells us a local user has read up to the given event_id in the room. """ @@ -115,14 +115,15 @@ def received_client_receipt(self, room_id, receipt_type, user_id, event_id): 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 = yield self._handle_new_receipts([receipt]) if not is_new: return - yield self.federation.send_read_receipt(receipt) + if not hidden: + yield self.federation.send_read_receipt(receipt) @defer.inlineCallbacks def get_receipts_for_room(self, room_id, to_key): diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 19bca6717f74..d421bc964c37 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -334,6 +334,7 @@ def ephemeral_by_room(self, sync_result_builder, now_token, since_token=None): """ sync_config = sync_result_builder.sync_config + user_id = sync_result_builder.sync_config.user.to_string() with Measure(self.clock, "ephemeral_by_room"): typing_key = since_token.typing_key if since_token else "0" @@ -376,7 +377,33 @@ def ephemeral_by_room(self, sync_result_builder, now_token, since_token=None): room_id = event["room_id"] # exclude room id, as above event_copy = {k: v for (k, v) in iteritems(event) if k != "room_id"} - ephemeral_by_room.setdefault(room_id, []).append(event_copy) + + # filter out receipts the user shouldn't see + content = event_copy.get("content", {}) + event_ids = content.keys() + reconstructed = event_copy.copy() + reconstructed["content"] = {} # clear old content + for event_id in event_ids: + m_read = content[event_id].get("m.read", None) + if m_read is None: + # clone it now - it's not something we can process + reconstructed["content"][event_id] = content[event_id] + continue + user_ids = m_read.keys() + for rr_user_id in user_ids: + data = m_read[rr_user_id] + hidden = data.get("hidden", False) + if rr_user_id == user_id or not hidden: + # append the key to the reconstructed receipt + new_content = reconstructed["content"] + if new_content.get(event_id, None) is None: + new_content[event_id] = {"m.read": {}} + ev_content = new_content[event_id]["m.read"] + if ev_content.get(rr_user_id, None) is None: + ev_content[rr_user_id] = {} + ev_content[rr_user_id] = data + if len(reconstructed["content"].keys()) > 0: + ephemeral_by_room.setdefault(room_id, []).append(reconstructed) return now_token, ephemeral_by_room diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index b3bf8567e11f..bd5c4e5a30ad 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -49,6 +49,7 @@ def on_POST(self, request, room_id): "m.read", user_id=requester.user.to_string(), event_id=read_event_id, + hidden=body.get("m.hidden", False), ) 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 0dab03d22775..9683d8143663 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -18,7 +18,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError -from synapse.http.servlet import RestServlet +from synapse.http.servlet import RestServlet, parse_json_object_from_request from ._base import client_patterns @@ -46,10 +46,16 @@ 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) + yield self.presence_handler.bump_presence_active_time(requester.user) yield 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=body.get("m.hidden", False), ) return 200, {}