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

Commit efd108b

Browse files
authored
Accept & store thread IDs for receipts (implement MSC3771). (#13782)
Updates the `/receipts` endpoint and receipt EDU handler to parse a `thread_id` from the body and insert it in the database.
1 parent 03c2bfb commit efd108b

File tree

17 files changed

+173
-41
lines changed

17 files changed

+173
-41
lines changed

changelog.d/13782.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for thread-specific receipts ([MSC3771](https://github.com/matrix-org/matrix-spec-proposals/pull/3771)).

synapse/config/experimental.py

+2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
8383
# MSC3786 (Add a default push rule to ignore m.room.server_acl events)
8484
self.msc3786_enabled: bool = experimental.get("msc3786_enabled", False)
8585

86+
# MSC3771: Thread read receipts
87+
self.msc3771_enabled: bool = experimental.get("msc3771_enabled", False)
8688
# MSC3772: A push rule for mutual relations.
8789
self.msc3772_enabled: bool = experimental.get("msc3772_enabled", False)
8890

synapse/handlers/receipts.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def __init__(self, hs: "HomeServer"):
6363
self.clock = self.hs.get_clock()
6464
self.state = hs.get_state_handler()
6565

66+
self._msc3771_enabled = hs.config.experimental.msc3771_enabled
67+
6668
async def _received_remote_receipt(self, origin: str, content: JsonDict) -> None:
6769
"""Called when we receive an EDU of type m.receipt from a remote HS."""
6870
receipts = []
@@ -91,13 +93,23 @@ async def _received_remote_receipt(self, origin: str, content: JsonDict) -> None
9193
)
9294
continue
9395

96+
# Check if these receipts apply to a thread.
97+
thread_id = None
98+
data = user_values.get("data", {})
99+
if self._msc3771_enabled and isinstance(data, dict):
100+
thread_id = data.get("thread_id")
101+
# If the thread ID is invalid, consider it missing.
102+
if not isinstance(thread_id, str):
103+
thread_id = None
104+
94105
receipts.append(
95106
ReadReceipt(
96107
room_id=room_id,
97108
receipt_type=receipt_type,
98109
user_id=user_id,
99110
event_ids=user_values["event_ids"],
100-
data=user_values.get("data", {}),
111+
thread_id=thread_id,
112+
data=data,
101113
)
102114
)
103115

@@ -114,6 +126,7 @@ async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool:
114126
receipt.receipt_type,
115127
receipt.user_id,
116128
receipt.event_ids,
129+
receipt.thread_id,
117130
receipt.data,
118131
)
119132

@@ -146,7 +159,12 @@ async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool:
146159
return True
147160

148161
async def received_client_receipt(
149-
self, room_id: str, receipt_type: str, user_id: str, event_id: str
162+
self,
163+
room_id: str,
164+
receipt_type: str,
165+
user_id: str,
166+
event_id: str,
167+
thread_id: Optional[str],
150168
) -> None:
151169
"""Called when a client tells us a local user has read up to the given
152170
event_id in the room.
@@ -156,6 +174,7 @@ async def received_client_receipt(
156174
receipt_type=receipt_type,
157175
user_id=user_id,
158176
event_ids=[event_id],
177+
thread_id=thread_id,
159178
data={"ts": int(self.clock.time_msec())},
160179
)
161180

synapse/replication/tcp/client.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,8 @@ async def _on_new_receipts(
427427
receipt.receipt_type,
428428
receipt.user_id,
429429
[receipt.event_id],
430-
receipt.data,
430+
thread_id=receipt.thread_id,
431+
data=receipt.data,
431432
)
432433
await self.federation_sender.send_read_receipt(receipt_info)
433434

synapse/replication/tcp/streams/_base.py

+1
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ class ReceiptsStreamRow:
361361
receipt_type: str
362362
user_id: str
363363
event_id: str
364+
thread_id: Optional[str]
364365
data: dict
365366

366367
NAME = "receipts"

synapse/rest/client/read_marker.py

+2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ async def on_POST(
8383
receipt_type,
8484
user_id=requester.user.to_string(),
8585
event_id=event_id,
86+
# Setting the thread ID is not possible with the /read_markers endpoint.
87+
thread_id=None,
8688
)
8789

8890
return 200, {}

synapse/rest/client/receipts.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def __init__(self, hs: "HomeServer"):
4949
ReceiptTypes.READ_PRIVATE,
5050
ReceiptTypes.FULLY_READ,
5151
}
52+
self._msc3771_enabled = hs.config.experimental.msc3771_enabled
5253

5354
async def on_POST(
5455
self, request: SynapseRequest, room_id: str, receipt_type: str, event_id: str
@@ -61,7 +62,17 @@ async def on_POST(
6162
f"Receipt type must be {', '.join(self._known_receipt_types)}",
6263
)
6364

64-
parse_json_object_from_request(request, allow_empty_body=False)
65+
body = parse_json_object_from_request(request)
66+
67+
# Pull the thread ID, if one exists.
68+
thread_id = None
69+
if self._msc3771_enabled:
70+
if "thread_id" in body:
71+
thread_id = body.get("thread_id")
72+
if not thread_id or not isinstance(thread_id, str):
73+
raise SynapseError(
74+
400, "thread_id field must be a non-empty string"
75+
)
6576

6677
await self.presence_handler.bump_presence_active_time(requester.user)
6778

@@ -77,6 +88,7 @@ async def on_POST(
7788
receipt_type,
7889
user_id=requester.user.to_string(),
7990
event_id=event_id,
91+
thread_id=thread_id,
8092
)
8193

8294
return 200, {}

synapse/rest/client/versions.py

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
103103
"org.matrix.msc3030": self.config.experimental.msc3030_enabled,
104104
# Adds support for thread relations, per MSC3440.
105105
"org.matrix.msc3440.stable": True, # TODO: remove when "v1.3" is added above
106+
# Support for thread read receipts.
107+
"org.matrix.msc3771": self.config.experimental.msc3771_enabled,
106108
# Allows moderators to fetch redacted event content as described in MSC2815
107109
"fi.mau.msc2815": self.config.experimental.msc2815_enabled,
108110
# Adds support for login token requests as per MSC3882

synapse/storage/database.py

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
"local_media_repository_thumbnails": "local_media_repository_thumbnails_method_idx",
9696
"remote_media_cache_thumbnails": "remote_media_repository_thumbnails_method_idx",
9797
"event_push_summary": "event_push_summary_unique_index",
98+
"receipts_linearized": "receipts_linearized_unique_index",
99+
"receipts_graph": "receipts_graph_unique_index",
98100
}
99101

100102

synapse/storage/databases/main/receipts.py

+64-23
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,9 @@ def _get_users_sent_receipts_between_txn(txn: LoggingTransaction) -> List[str]:
540540

541541
async def get_all_updated_receipts(
542542
self, instance_name: str, last_id: int, current_id: int, limit: int
543-
) -> Tuple[List[Tuple[int, list]], int, bool]:
543+
) -> Tuple[
544+
List[Tuple[int, Tuple[str, str, str, str, Optional[str], JsonDict]]], int, bool
545+
]:
544546
"""Get updates for receipts replication stream.
545547
546548
Args:
@@ -567,9 +569,13 @@ async def get_all_updated_receipts(
567569

568570
def get_all_updated_receipts_txn(
569571
txn: LoggingTransaction,
570-
) -> Tuple[List[Tuple[int, list]], int, bool]:
572+
) -> Tuple[
573+
List[Tuple[int, Tuple[str, str, str, str, Optional[str], JsonDict]]],
574+
int,
575+
bool,
576+
]:
571577
sql = """
572-
SELECT stream_id, room_id, receipt_type, user_id, event_id, data
578+
SELECT stream_id, room_id, receipt_type, user_id, event_id, thread_id, data
573579
FROM receipts_linearized
574580
WHERE ? < stream_id AND stream_id <= ?
575581
ORDER BY stream_id ASC
@@ -578,8 +584,8 @@ def get_all_updated_receipts_txn(
578584
txn.execute(sql, (last_id, current_id, limit))
579585

580586
updates = cast(
581-
List[Tuple[int, list]],
582-
[(r[0], r[1:5] + (db_to_json(r[5]),)) for r in txn],
587+
List[Tuple[int, Tuple[str, str, str, str, Optional[str], JsonDict]]],
588+
[(r[0], r[1:6] + (db_to_json(r[6]),)) for r in txn],
583589
)
584590

585591
limited = False
@@ -631,6 +637,7 @@ def _insert_linearized_receipt_txn(
631637
receipt_type: str,
632638
user_id: str,
633639
event_id: str,
640+
thread_id: Optional[str],
634641
data: JsonDict,
635642
stream_id: int,
636643
) -> Optional[int]:
@@ -657,12 +664,27 @@ def _insert_linearized_receipt_txn(
657664
# We don't want to clobber receipts for more recent events, so we
658665
# have to compare orderings of existing receipts
659666
if stream_ordering is not None:
660-
sql = (
661-
"SELECT stream_ordering, event_id FROM events"
662-
" INNER JOIN receipts_linearized AS r USING (event_id, room_id)"
663-
" WHERE r.room_id = ? AND r.receipt_type = ? AND r.user_id = ?"
667+
if thread_id is None:
668+
thread_clause = "r.thread_id IS NULL"
669+
thread_args: Tuple[str, ...] = ()
670+
else:
671+
thread_clause = "r.thread_id = ?"
672+
thread_args = (thread_id,)
673+
674+
sql = f"""
675+
SELECT stream_ordering, event_id FROM events
676+
INNER JOIN receipts_linearized AS r USING (event_id, room_id)
677+
WHERE r.room_id = ? AND r.receipt_type = ? AND r.user_id = ? AND {thread_clause}
678+
"""
679+
txn.execute(
680+
sql,
681+
(
682+
room_id,
683+
receipt_type,
684+
user_id,
685+
)
686+
+ thread_args,
664687
)
665-
txn.execute(sql, (room_id, receipt_type, user_id))
666688

667689
for so, eid in txn:
668690
if int(so) >= stream_ordering:
@@ -682,21 +704,28 @@ def _insert_linearized_receipt_txn(
682704
self._receipts_stream_cache.entity_has_changed, room_id, stream_id
683705
)
684706

707+
keyvalues = {
708+
"room_id": room_id,
709+
"receipt_type": receipt_type,
710+
"user_id": user_id,
711+
}
712+
where_clause = ""
713+
if thread_id is None:
714+
where_clause = "thread_id IS NULL"
715+
else:
716+
keyvalues["thread_id"] = thread_id
717+
685718
self.db_pool.simple_upsert_txn(
686719
txn,
687720
table="receipts_linearized",
688-
keyvalues={
689-
"room_id": room_id,
690-
"receipt_type": receipt_type,
691-
"user_id": user_id,
692-
},
721+
keyvalues=keyvalues,
693722
values={
694723
"stream_id": stream_id,
695724
"event_id": event_id,
696725
"event_stream_ordering": stream_ordering,
697726
"data": json_encoder.encode(data),
698-
"thread_id": None,
699727
},
728+
where_clause=where_clause,
700729
# receipts_linearized has a unique constraint on
701730
# (user_id, room_id, receipt_type), so no need to lock
702731
lock=False,
@@ -748,6 +777,7 @@ async def insert_receipt(
748777
receipt_type: str,
749778
user_id: str,
750779
event_ids: List[str],
780+
thread_id: Optional[str],
751781
data: dict,
752782
) -> Optional[Tuple[int, int]]:
753783
"""Insert a receipt, either from local client or remote server.
@@ -780,6 +810,7 @@ async def insert_receipt(
780810
receipt_type,
781811
user_id,
782812
linearized_event_id,
813+
thread_id,
783814
data,
784815
stream_id=stream_id,
785816
# Read committed is actually beneficial here because we check for a receipt with
@@ -794,7 +825,8 @@ async def insert_receipt(
794825

795826
now = self._clock.time_msec()
796827
logger.debug(
797-
"RR for event %s in %s (%i ms old)",
828+
"Receipt %s for event %s in %s (%i ms old)",
829+
receipt_type,
798830
linearized_event_id,
799831
room_id,
800832
now - event_ts,
@@ -807,6 +839,7 @@ async def insert_receipt(
807839
receipt_type,
808840
user_id,
809841
event_ids,
842+
thread_id,
810843
data,
811844
)
812845

@@ -821,6 +854,7 @@ def _insert_graph_receipt_txn(
821854
receipt_type: str,
822855
user_id: str,
823856
event_ids: List[str],
857+
thread_id: Optional[str],
824858
data: JsonDict,
825859
) -> None:
826860
assert self._can_write_to_receipts
@@ -832,19 +866,26 @@ def _insert_graph_receipt_txn(
832866
# FIXME: This shouldn't invalidate the whole cache
833867
txn.call_after(self._get_linearized_receipts_for_room.invalidate, (room_id,))
834868

869+
keyvalues = {
870+
"room_id": room_id,
871+
"receipt_type": receipt_type,
872+
"user_id": user_id,
873+
}
874+
where_clause = ""
875+
if thread_id is None:
876+
where_clause = "thread_id IS NULL"
877+
else:
878+
keyvalues["thread_id"] = thread_id
879+
835880
self.db_pool.simple_upsert_txn(
836881
txn,
837882
table="receipts_graph",
838-
keyvalues={
839-
"room_id": room_id,
840-
"receipt_type": receipt_type,
841-
"user_id": user_id,
842-
},
883+
keyvalues=keyvalues,
843884
values={
844885
"event_ids": json_encoder.encode(event_ids),
845886
"data": json_encoder.encode(data),
846-
"thread_id": None,
847887
},
888+
where_clause=where_clause,
848889
# receipts_graph has a unique constraint on
849890
# (user_id, room_id, receipt_type), so no need to lock
850891
lock=False,

synapse/types.py

+1
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,7 @@ class ReadReceipt:
835835
receipt_type: str
836836
user_id: str
837837
event_ids: List[str]
838+
thread_id: Optional[str]
838839
data: JsonDict
839840

840841

tests/federation/test_federation_sender.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ def test_send_receipts(self):
4949

5050
sender = self.hs.get_federation_sender()
5151
receipt = ReadReceipt(
52-
"room_id", "m.read", "user_id", ["event_id"], {"ts": 1234}
52+
"room_id",
53+
"m.read",
54+
"user_id",
55+
["event_id"],
56+
thread_id=None,
57+
data={"ts": 1234},
5358
)
5459
self.successResultOf(defer.ensureDeferred(sender.send_read_receipt(receipt)))
5560

@@ -89,7 +94,12 @@ def test_send_receipts_with_backoff(self):
8994

9095
sender = self.hs.get_federation_sender()
9196
receipt = ReadReceipt(
92-
"room_id", "m.read", "user_id", ["event_id"], {"ts": 1234}
97+
"room_id",
98+
"m.read",
99+
"user_id",
100+
["event_id"],
101+
thread_id=None,
102+
data={"ts": 1234},
93103
)
94104
self.successResultOf(defer.ensureDeferred(sender.send_read_receipt(receipt)))
95105

@@ -121,7 +131,12 @@ def test_send_receipts_with_backoff(self):
121131

122132
# send the second RR
123133
receipt = ReadReceipt(
124-
"room_id", "m.read", "user_id", ["other_id"], {"ts": 1234}
134+
"room_id",
135+
"m.read",
136+
"user_id",
137+
["other_id"],
138+
thread_id=None,
139+
data={"ts": 1234},
125140
)
126141
self.successResultOf(defer.ensureDeferred(sender.send_read_receipt(receipt)))
127142
self.pump()

0 commit comments

Comments
 (0)