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

Commit 86c5a71

Browse files
baboliviersquahtx
andauthored
Implement MSC3912: Relation-based redactions (#14260)
Co-authored-by: Sean Quah <[email protected]>
1 parent e5cd278 commit 86c5a71

File tree

10 files changed

+486
-28
lines changed

10 files changed

+486
-28
lines changed

changelog.d/14260.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add experimental support for [MSC3912](https://github.com/matrix-org/matrix-spec-proposals/pull/3912): Relation-based redactions.

synapse/api/constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class EventTypes:
125125
MSC2716_BATCH: Final = "org.matrix.msc2716.batch"
126126
MSC2716_MARKER: Final = "org.matrix.msc2716.marker"
127127

128+
Reaction: Final = "m.reaction"
129+
128130

129131
class ToDeviceEventTypes:
130132
RoomKeyRequest: Final = "m.room_key_request"

synapse/config/experimental.py

+3
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
128128
self.msc3886_endpoint: Optional[str] = experimental.get(
129129
"msc3886_endpoint", None
130130
)
131+
132+
# MSC3912: Relation-based redactions.
133+
self.msc3912_enabled: bool = experimental.get("msc3912_enabled", False)

synapse/handlers/message.py

+38-9
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,36 @@ async def deduplicate_state_event(
877877
return prev_event
878878
return None
879879

880+
async def get_event_from_transaction(
881+
self,
882+
requester: Requester,
883+
txn_id: str,
884+
room_id: str,
885+
) -> Optional[EventBase]:
886+
"""For the given transaction ID and room ID, check if there is a matching event.
887+
If so, fetch it and return it.
888+
889+
Args:
890+
requester: The requester making the request in the context of which we want
891+
to fetch the event.
892+
txn_id: The transaction ID.
893+
room_id: The room ID.
894+
895+
Returns:
896+
An event if one could be found, None otherwise.
897+
"""
898+
if requester.access_token_id:
899+
existing_event_id = await self.store.get_event_id_from_transaction_id(
900+
room_id,
901+
requester.user.to_string(),
902+
requester.access_token_id,
903+
txn_id,
904+
)
905+
if existing_event_id:
906+
return await self.store.get_event(existing_event_id)
907+
908+
return None
909+
880910
async def create_and_send_nonmember_event(
881911
self,
882912
requester: Requester,
@@ -956,18 +986,17 @@ async def create_and_send_nonmember_event(
956986
# extremities to pile up, which in turn leads to state resolution
957987
# taking longer.
958988
async with self.limiter.queue(event_dict["room_id"]):
959-
if txn_id and requester.access_token_id:
960-
existing_event_id = await self.store.get_event_id_from_transaction_id(
961-
event_dict["room_id"],
962-
requester.user.to_string(),
963-
requester.access_token_id,
964-
txn_id,
989+
if txn_id:
990+
event = await self.get_event_from_transaction(
991+
requester, txn_id, event_dict["room_id"]
965992
)
966-
if existing_event_id:
967-
event = await self.store.get_event(existing_event_id)
993+
if event:
968994
# we know it was persisted, so must have a stream ordering
969995
assert event.internal_metadata.stream_ordering
970-
return event, event.internal_metadata.stream_ordering
996+
return (
997+
event,
998+
event.internal_metadata.stream_ordering,
999+
)
9711000

9721001
event, context = await self.create_event(
9731002
requester,

synapse/handlers/relations.py

+55-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import attr
1919

20-
from synapse.api.constants import RelationTypes
20+
from synapse.api.constants import EventTypes, RelationTypes
2121
from synapse.api.errors import SynapseError
2222
from synapse.events import EventBase, relation_from_event
2323
from synapse.logging.opentracing import trace
@@ -75,6 +75,7 @@ def __init__(self, hs: "HomeServer"):
7575
self._clock = hs.get_clock()
7676
self._event_handler = hs.get_event_handler()
7777
self._event_serializer = hs.get_event_client_serializer()
78+
self._event_creation_handler = hs.get_event_creation_handler()
7879

7980
async def get_relations(
8081
self,
@@ -205,6 +206,59 @@ async def get_relations_for_event(
205206

206207
return related_events, next_token
207208

209+
async def redact_events_related_to(
210+
self,
211+
requester: Requester,
212+
event_id: str,
213+
initial_redaction_event: EventBase,
214+
relation_types: List[str],
215+
) -> None:
216+
"""Redacts all events related to the given event ID with one of the given
217+
relation types.
218+
219+
This method is expected to be called when redacting the event referred to by
220+
the given event ID.
221+
222+
If an event cannot be redacted (e.g. because of insufficient permissions), log
223+
the error and try to redact the next one.
224+
225+
Args:
226+
requester: The requester to redact events on behalf of.
227+
event_id: The event IDs to look and redact relations of.
228+
initial_redaction_event: The redaction for the event referred to by
229+
event_id.
230+
relation_types: The types of relations to look for.
231+
232+
Raises:
233+
ShadowBanError if the requester is shadow-banned
234+
"""
235+
related_event_ids = (
236+
await self._main_store.get_all_relations_for_event_with_types(
237+
event_id, relation_types
238+
)
239+
)
240+
241+
for related_event_id in related_event_ids:
242+
try:
243+
await self._event_creation_handler.create_and_send_nonmember_event(
244+
requester,
245+
{
246+
"type": EventTypes.Redaction,
247+
"content": initial_redaction_event.content,
248+
"room_id": initial_redaction_event.room_id,
249+
"sender": requester.user.to_string(),
250+
"redacts": related_event_id,
251+
},
252+
ratelimit=False,
253+
)
254+
except SynapseError as e:
255+
logger.warning(
256+
"Failed to redact event %s (related to event %s): %s",
257+
related_event_id,
258+
event_id,
259+
e.msg,
260+
)
261+
208262
async def get_annotations_for_event(
209263
self,
210264
event_id: str,

synapse/rest/client/room.py

+43-14
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from synapse.http.site import SynapseRequest
5353
from synapse.logging.context import make_deferred_yieldable, run_in_background
5454
from synapse.logging.opentracing import set_tag
55+
from synapse.metrics.background_process_metrics import run_as_background_process
5556
from synapse.rest.client._base import client_patterns
5657
from synapse.rest.client.transactions import HttpTransactionCache
5758
from synapse.storage.state import StateFilter
@@ -1029,6 +1030,8 @@ def __init__(self, hs: "HomeServer"):
10291030
super().__init__(hs)
10301031
self.event_creation_handler = hs.get_event_creation_handler()
10311032
self.auth = hs.get_auth()
1033+
self._relation_handler = hs.get_relations_handler()
1034+
self._msc3912_enabled = hs.config.experimental.msc3912_enabled
10321035

10331036
def register(self, http_server: HttpServer) -> None:
10341037
PATTERNS = "/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)"
@@ -1045,20 +1048,46 @@ async def on_POST(
10451048
content = parse_json_object_from_request(request)
10461049

10471050
try:
1048-
(
1049-
event,
1050-
_,
1051-
) = await self.event_creation_handler.create_and_send_nonmember_event(
1052-
requester,
1053-
{
1054-
"type": EventTypes.Redaction,
1055-
"content": content,
1056-
"room_id": room_id,
1057-
"sender": requester.user.to_string(),
1058-
"redacts": event_id,
1059-
},
1060-
txn_id=txn_id,
1061-
)
1051+
with_relations = None
1052+
if self._msc3912_enabled and "org.matrix.msc3912.with_relations" in content:
1053+
with_relations = content["org.matrix.msc3912.with_relations"]
1054+
del content["org.matrix.msc3912.with_relations"]
1055+
1056+
# Check if there's an existing event for this transaction now (even though
1057+
# create_and_send_nonmember_event also does it) because, if there's one,
1058+
# then we want to skip the call to redact_events_related_to.
1059+
event = None
1060+
if txn_id:
1061+
event = await self.event_creation_handler.get_event_from_transaction(
1062+
requester, txn_id, room_id
1063+
)
1064+
1065+
if event is None:
1066+
(
1067+
event,
1068+
_,
1069+
) = await self.event_creation_handler.create_and_send_nonmember_event(
1070+
requester,
1071+
{
1072+
"type": EventTypes.Redaction,
1073+
"content": content,
1074+
"room_id": room_id,
1075+
"sender": requester.user.to_string(),
1076+
"redacts": event_id,
1077+
},
1078+
txn_id=txn_id,
1079+
)
1080+
1081+
if with_relations:
1082+
run_as_background_process(
1083+
"redact_related_events",
1084+
self._relation_handler.redact_events_related_to,
1085+
requester=requester,
1086+
event_id=event_id,
1087+
initial_redaction_event=event,
1088+
relation_types=with_relations,
1089+
)
1090+
10621091
event_id = event.event_id
10631092
except ShadowBanError:
10641093
event_id = "$" + random_string(43)

synapse/rest/client/versions.py

+2
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
119119
# Adds support for simple HTTP rendezvous as per MSC3886
120120
"org.matrix.msc3886": self.config.experimental.msc3886_endpoint
121121
is not None,
122+
# Adds support for relation-based redactions as per MSC3912.
123+
"org.matrix.msc3912": self.config.experimental.msc3912_enabled,
122124
},
123125
},
124126
)

synapse/storage/databases/main/relations.py

+36
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,42 @@ def _get_recent_references_for_event_txn(
295295
"get_recent_references_for_event", _get_recent_references_for_event_txn
296296
)
297297

298+
async def get_all_relations_for_event_with_types(
299+
self,
300+
event_id: str,
301+
relation_types: List[str],
302+
) -> List[str]:
303+
"""Get the event IDs of all events that have a relation to the given event with
304+
one of the given relation types.
305+
306+
Args:
307+
event_id: The event for which to look for related events.
308+
relation_types: The types of relations to look for.
309+
310+
Returns:
311+
A list of the IDs of the events that relate to the given event with one of
312+
the given relation types.
313+
"""
314+
315+
def get_all_relation_ids_for_event_with_types_txn(
316+
txn: LoggingTransaction,
317+
) -> List[str]:
318+
rows = self.db_pool.simple_select_many_txn(
319+
txn=txn,
320+
table="event_relations",
321+
column="relation_type",
322+
iterable=relation_types,
323+
keyvalues={"relates_to_id": event_id},
324+
retcols=["event_id"],
325+
)
326+
327+
return [row["event_id"] for row in rows]
328+
329+
return await self.db_pool.runInteraction(
330+
desc="get_all_relation_ids_for_event_with_types",
331+
func=get_all_relation_ids_for_event_with_types_txn,
332+
)
333+
298334
async def event_includes_relation(self, event_id: str) -> bool:
299335
"""Check if the given event relates to another event.
300336

0 commit comments

Comments
 (0)