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

Commit 116a4c8

Browse files
Implement changes to MSC2285 (hidden read receipts) (#12168)
* Changes hidden read receipts to be a separate receipt type (instead of a field on `m.read`). * Updates the `/receipts` endpoint to accept `m.fully_read`.
1 parent 332cce8 commit 116a4c8

File tree

12 files changed

+647
-186
lines changed

12 files changed

+647
-186
lines changed

changelog.d/12168.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement [changes](https://github.com/matrix-org/matrix-spec-proposals/pull/2285/commits/4a77139249c2e830aec3c7d6bd5501a514d1cc27) to [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-spec-proposals/pull/2285). Contributed by @SimonBrandner.

synapse/api/constants.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,5 @@ class GuestAccess:
255255

256256
class ReceiptTypes:
257257
READ: Final = "m.read"
258-
259-
260-
class ReadReceiptEventFields:
261-
MSC2285_HIDDEN: Final = "org.matrix.msc2285.hidden"
258+
READ_PRIVATE: Final = "org.matrix.msc2285.read.private"
259+
FULLY_READ: Final = "m.fully_read"

synapse/handlers/receipts.py

+27-38
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import logging
1515
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
1616

17-
from synapse.api.constants import ReadReceiptEventFields, ReceiptTypes
17+
from synapse.api.constants import ReceiptTypes
1818
from synapse.appservice import ApplicationService
1919
from synapse.streams import EventSource
2020
from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id
@@ -112,7 +112,7 @@ async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool:
112112
)
113113

114114
if not res:
115-
# res will be None if this read receipt is 'old'
115+
# res will be None if this receipt is 'old'
116116
continue
117117

118118
stream_id, max_persisted_id = res
@@ -138,7 +138,7 @@ async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool:
138138
return True
139139

140140
async def received_client_receipt(
141-
self, room_id: str, receipt_type: str, user_id: str, event_id: str, hidden: bool
141+
self, room_id: str, receipt_type: str, user_id: str, event_id: str
142142
) -> None:
143143
"""Called when a client tells us a local user has read up to the given
144144
event_id in the room.
@@ -148,16 +148,14 @@ async def received_client_receipt(
148148
receipt_type=receipt_type,
149149
user_id=user_id,
150150
event_ids=[event_id],
151-
data={"ts": int(self.clock.time_msec()), "hidden": hidden},
151+
data={"ts": int(self.clock.time_msec())},
152152
)
153153

154154
is_new = await self._handle_new_receipts([receipt])
155155
if not is_new:
156156
return
157157

158-
if self.federation_sender and not (
159-
self.hs.config.experimental.msc2285_enabled and hidden
160-
):
158+
if self.federation_sender and receipt_type != ReceiptTypes.READ_PRIVATE:
161159
await self.federation_sender.send_read_receipt(receipt)
162160

163161

@@ -168,6 +166,13 @@ def __init__(self, hs: "HomeServer"):
168166

169167
@staticmethod
170168
def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]:
169+
"""
170+
This method takes in what is returned by
171+
get_linearized_receipts_for_rooms() and goes through read receipts
172+
filtering out m.read.private receipts if they were not sent by the
173+
current user.
174+
"""
175+
171176
visible_events = []
172177

173178
# filter out hidden receipts the user shouldn't see
@@ -176,37 +181,21 @@ def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]:
176181
new_event = event.copy()
177182
new_event["content"] = {}
178183

179-
for event_id in content.keys():
180-
event_content = content.get(event_id, {})
181-
m_read = event_content.get(ReceiptTypes.READ, {})
182-
183-
# If m_read is missing copy over the original event_content as there is nothing to process here
184-
if not m_read:
185-
new_event["content"][event_id] = event_content.copy()
186-
continue
187-
188-
new_users = {}
189-
for rr_user_id, user_rr in m_read.items():
190-
try:
191-
hidden = user_rr.get("hidden")
192-
except AttributeError:
193-
# Due to https://github.com/matrix-org/synapse/issues/10376
194-
# there are cases where user_rr is a string, in those cases
195-
# we just ignore the read receipt
196-
continue
197-
198-
if hidden is not True or rr_user_id == user_id:
199-
new_users[rr_user_id] = user_rr.copy()
200-
# If hidden has a value replace hidden with the correct prefixed key
201-
if hidden is not None:
202-
new_users[rr_user_id].pop("hidden")
203-
new_users[rr_user_id][
204-
ReadReceiptEventFields.MSC2285_HIDDEN
205-
] = hidden
206-
207-
# Set new users unless empty
208-
if len(new_users.keys()) > 0:
209-
new_event["content"][event_id] = {ReceiptTypes.READ: new_users}
184+
for event_id, event_content in content.items():
185+
receipt_event = {}
186+
for receipt_type, receipt_content in event_content.items():
187+
if receipt_type == ReceiptTypes.READ_PRIVATE:
188+
user_rr = receipt_content.get(user_id, None)
189+
if user_rr:
190+
receipt_event[ReceiptTypes.READ_PRIVATE] = {
191+
user_id: user_rr.copy()
192+
}
193+
else:
194+
receipt_event[receipt_type] = receipt_content.copy()
195+
196+
# Only include the receipt event if it is non-empty.
197+
if receipt_event:
198+
new_event["content"][event_id] = receipt_event
210199

211200
# Append new_event to visible_events unless empty
212201
if len(new_event["content"].keys()) > 0:

synapse/handlers/sync.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,7 @@ async def unread_notifs_for_room_id(
10451045
last_unread_event_id = await self.store.get_last_receipt_event_id_for_user(
10461046
user_id=sync_config.user.to_string(),
10471047
room_id=room_id,
1048-
receipt_type=ReceiptTypes.READ,
1048+
receipt_types=(ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE),
10491049
)
10501050

10511051
return await self.store.get_unread_event_push_actions_by_room_for_user(

synapse/push/push_tools.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ async def get_badge_count(store: DataStore, user_id: str, group_by_room: bool) -
2424
invites = await store.get_invited_rooms_for_local_user(user_id)
2525
joins = await store.get_rooms_for_user(user_id)
2626

27-
my_receipts_by_room = await store.get_receipts_for_user(user_id, ReceiptTypes.READ)
27+
my_receipts_by_room = await store.get_receipts_for_user(
28+
user_id, (ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE)
29+
)
2830

2931
badge = len(invites)
3032

synapse/rest/client/notifications.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
5858
)
5959

6060
receipts_by_room = await self.store.get_receipts_for_user_with_orderings(
61-
user_id, ReceiptTypes.READ
61+
user_id, [ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE]
6262
)
6363

6464
notif_event_ids = [pa.event_id for pa in push_actions]

synapse/rest/client/read_marker.py

+22-10
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
import logging
1616
from typing import TYPE_CHECKING, Tuple
1717

18-
from synapse.api.constants import ReadReceiptEventFields, ReceiptTypes
19-
from synapse.api.errors import Codes, SynapseError
18+
from synapse.api.constants import ReceiptTypes
19+
from synapse.api.errors import SynapseError
2020
from synapse.http.server import HttpServer
2121
from synapse.http.servlet import RestServlet, parse_json_object_from_request
2222
from synapse.http.site import SynapseRequest
@@ -36,6 +36,7 @@ class ReadMarkerRestServlet(RestServlet):
3636
def __init__(self, hs: "HomeServer"):
3737
super().__init__()
3838
self.auth = hs.get_auth()
39+
self.config = hs.config
3940
self.receipts_handler = hs.get_receipts_handler()
4041
self.read_marker_handler = hs.get_read_marker_handler()
4142
self.presence_handler = hs.get_presence_handler()
@@ -48,27 +49,38 @@ async def on_POST(
4849
await self.presence_handler.bump_presence_active_time(requester.user)
4950

5051
body = parse_json_object_from_request(request)
51-
read_event_id = body.get(ReceiptTypes.READ, None)
52-
hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False)
5352

54-
if not isinstance(hidden, bool):
53+
valid_receipt_types = {ReceiptTypes.READ, ReceiptTypes.FULLY_READ}
54+
if self.config.experimental.msc2285_enabled:
55+
valid_receipt_types.add(ReceiptTypes.READ_PRIVATE)
56+
57+
if set(body.keys()) > valid_receipt_types:
5558
raise SynapseError(
5659
400,
57-
"Param %s must be a boolean, if given"
58-
% ReadReceiptEventFields.MSC2285_HIDDEN,
59-
Codes.BAD_JSON,
60+
"Receipt type must be 'm.read', 'org.matrix.msc2285.read.private' or 'm.fully_read'"
61+
if self.config.experimental.msc2285_enabled
62+
else "Receipt type must be 'm.read' or 'm.fully_read'",
6063
)
6164

65+
read_event_id = body.get(ReceiptTypes.READ, None)
6266
if read_event_id:
6367
await self.receipts_handler.received_client_receipt(
6468
room_id,
6569
ReceiptTypes.READ,
6670
user_id=requester.user.to_string(),
6771
event_id=read_event_id,
68-
hidden=hidden,
6972
)
7073

71-
read_marker_event_id = body.get("m.fully_read", None)
74+
read_private_event_id = body.get(ReceiptTypes.READ_PRIVATE, None)
75+
if read_private_event_id and self.config.experimental.msc2285_enabled:
76+
await self.receipts_handler.received_client_receipt(
77+
room_id,
78+
ReceiptTypes.READ_PRIVATE,
79+
user_id=requester.user.to_string(),
80+
event_id=read_private_event_id,
81+
)
82+
83+
read_marker_event_id = body.get(ReceiptTypes.FULLY_READ, None)
7284
if read_marker_event_id:
7385
await self.read_marker_handler.received_client_read_marker(
7486
room_id,

synapse/rest/client/receipts.py

+31-20
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
import re
1717
from typing import TYPE_CHECKING, Tuple
1818

19-
from synapse.api.constants import ReadReceiptEventFields, ReceiptTypes
20-
from synapse.api.errors import Codes, SynapseError
19+
from synapse.api.constants import ReceiptTypes
20+
from synapse.api.errors import SynapseError
2121
from synapse.http import get_request_user_agent
2222
from synapse.http.server import HttpServer
2323
from synapse.http.servlet import RestServlet, parse_json_object_from_request
@@ -46,14 +46,27 @@ def __init__(self, hs: "HomeServer"):
4646
self.hs = hs
4747
self.auth = hs.get_auth()
4848
self.receipts_handler = hs.get_receipts_handler()
49+
self.read_marker_handler = hs.get_read_marker_handler()
4950
self.presence_handler = hs.get_presence_handler()
5051

5152
async def on_POST(
5253
self, request: SynapseRequest, room_id: str, receipt_type: str, event_id: str
5354
) -> Tuple[int, JsonDict]:
5455
requester = await self.auth.get_user_by_req(request)
5556

56-
if receipt_type != ReceiptTypes.READ:
57+
if self.hs.config.experimental.msc2285_enabled and receipt_type not in [
58+
ReceiptTypes.READ,
59+
ReceiptTypes.READ_PRIVATE,
60+
ReceiptTypes.FULLY_READ,
61+
]:
62+
raise SynapseError(
63+
400,
64+
"Receipt type must be 'm.read', 'org.matrix.msc2285.read.private' or 'm.fully_read'",
65+
)
66+
elif (
67+
not self.hs.config.experimental.msc2285_enabled
68+
and receipt_type != ReceiptTypes.READ
69+
):
5770
raise SynapseError(400, "Receipt type must be 'm.read'")
5871

5972
# Do not allow older SchildiChat and Element Android clients (prior to Element/1.[012].x) to send an empty body.
@@ -62,26 +75,24 @@ async def on_POST(
6275
if "Android" in user_agent:
6376
if pattern.match(user_agent) or "Riot" in user_agent:
6477
allow_empty_body = True
65-
body = parse_json_object_from_request(request, allow_empty_body)
66-
hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False)
67-
68-
if not isinstance(hidden, bool):
69-
raise SynapseError(
70-
400,
71-
"Param %s must be a boolean, if given"
72-
% ReadReceiptEventFields.MSC2285_HIDDEN,
73-
Codes.BAD_JSON,
74-
)
78+
# This call makes sure possible empty body is handled correctly
79+
parse_json_object_from_request(request, allow_empty_body)
7580

7681
await self.presence_handler.bump_presence_active_time(requester.user)
7782

78-
await self.receipts_handler.received_client_receipt(
79-
room_id,
80-
receipt_type,
81-
user_id=requester.user.to_string(),
82-
event_id=event_id,
83-
hidden=hidden,
84-
)
83+
if receipt_type == ReceiptTypes.FULLY_READ:
84+
await self.read_marker_handler.received_client_read_marker(
85+
room_id,
86+
user_id=requester.user.to_string(),
87+
event_id=event_id,
88+
)
89+
else:
90+
await self.receipts_handler.received_client_receipt(
91+
room_id,
92+
receipt_type,
93+
user_id=requester.user.to_string(),
94+
event_id=event_id,
95+
)
8596

8697
return 200, {}
8798

0 commit comments

Comments
 (0)