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

Implement MSC3912: Relation-based redactions #14260

Merged
merged 9 commits into from
Nov 3, 2022
1 change: 1 addition & 0 deletions changelog.d/14260.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add experimental support for [MSC3912](https://github.com/matrix-org/matrix-spec-proposals/pull/3912): Relation-based redactions.
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
self.msc3886_endpoint: Optional[str] = experimental.get(
"msc3886_endpoint", None
)

# MSC3912: Relation-based redactions.
self.msc3912_enabled: bool = experimental.get("msc3912_enabled", False)
47 changes: 38 additions & 9 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,36 @@ async def deduplicate_state_event(
return prev_event
return None

async def get_event_from_transaction(
self,
requester: Requester,
txn_id: str,
room_id: str,
) -> Optional[EventBase]:
"""For the given transaction ID and room ID, check if there is a matching event.
If so, fetch it and return it.

Args:
requester: The requester making the request in the context of which we want
to fetch the event.
txn_id: The transaction ID.
room_id: The room ID.

Returns:
An event if one could be found, None otherwise.
"""
if requester.access_token_id:
existing_event_id = await self.store.get_event_id_from_transaction_id(
room_id,
requester.user.to_string(),
requester.access_token_id,
txn_id,
)
if existing_event_id:
return await self.store.get_event(existing_event_id)

return None

async def create_and_send_nonmember_event(
self,
requester: Requester,
Expand Down Expand Up @@ -956,18 +986,17 @@ async def create_and_send_nonmember_event(
# extremities to pile up, which in turn leads to state resolution
# taking longer.
async with self.limiter.queue(event_dict["room_id"]):
if txn_id and requester.access_token_id:
existing_event_id = await self.store.get_event_id_from_transaction_id(
event_dict["room_id"],
requester.user.to_string(),
requester.access_token_id,
txn_id,
if txn_id:
event = await self.get_event_from_transaction(
requester, txn_id, event_dict["room_id"]
)
if existing_event_id:
event = await self.store.get_event(existing_event_id)
if event:
# we know it was persisted, so must have a stream ordering
assert event.internal_metadata.stream_ordering
return event, event.internal_metadata.stream_ordering
return (
event,
event.internal_metadata.stream_ordering,
)

event, context = await self.create_event(
requester,
Expand Down
56 changes: 55 additions & 1 deletion synapse/handlers/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import attr

from synapse.api.constants import RelationTypes
from synapse.api.constants import EventTypes, RelationTypes
from synapse.api.errors import SynapseError
from synapse.events import EventBase, relation_from_event
from synapse.logging.opentracing import trace
Expand Down Expand Up @@ -75,6 +75,7 @@ def __init__(self, hs: "HomeServer"):
self._clock = hs.get_clock()
self._event_handler = hs.get_event_handler()
self._event_serializer = hs.get_event_client_serializer()
self._event_creation_handler = hs.get_event_creation_handler()

async def get_relations(
self,
Expand Down Expand Up @@ -205,6 +206,59 @@ async def get_relations_for_event(

return related_events, next_token

async def redact_events_related_to(
self,
requester: Requester,
event_id: str,
initial_redaction_event: EventBase,
relation_types: List[str],
) -> None:
"""Redacts all events related to the given event ID with one of the given
relation types.

This method is expected to be called when redacting the event referred to by
the given event ID.

If an event cannot be redacted (e.g. because of insufficient permissions), log
the error and try to redact the next one.

Args:
requester: The requester to redact events on behalf of.
event_id: The event IDs to look and redact relations of.
initial_redaction_event: The redaction for the event referred to by
event_id.
relation_types: The types of relations to look for.

Raises:
ShadowBanError if the requester is shadow-banned
"""
related_event_ids = (
await self._main_store.get_all_relations_for_event_with_types(
event_id, relation_types
)
)

for related_event_id in related_event_ids:
try:
await self._event_creation_handler.create_and_send_nonmember_event(
requester,
{
"type": EventTypes.Redaction,
"content": initial_redaction_event.content,
"room_id": initial_redaction_event.room_id,
"sender": requester.user.to_string(),
"redacts": related_event_id,
},
ratelimit=False,
)
except SynapseError as e:
logger.warning(
"Failed to redact event %s (related to event %s): %s",
related_event_id,
event_id,
e.msg,
)

async def get_annotations_for_event(
self,
event_id: str,
Expand Down
55 changes: 41 additions & 14 deletions synapse/rest/client/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,8 @@ def __init__(self, hs: "HomeServer"):
super().__init__(hs)
self.event_creation_handler = hs.get_event_creation_handler()
self.auth = hs.get_auth()
self._relation_handler = hs.get_relations_handler()
self._msc3912_enabled = hs.config.experimental.msc3912_enabled

def register(self, http_server: HttpServer) -> None:
PATTERNS = "/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)"
Expand All @@ -1045,20 +1047,45 @@ async def on_POST(
content = parse_json_object_from_request(request)

try:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
requester,
{
"type": EventTypes.Redaction,
"content": content,
"room_id": room_id,
"sender": requester.user.to_string(),
"redacts": event_id,
},
txn_id=txn_id,
)
with_relations = None
if self._msc3912_enabled and "org.matrix.msc3912.with_relations" in content:
with_relations = content["org.matrix.msc3912.with_relations"]
del content["org.matrix.msc3912.with_relations"]

# Check if there's an existing event for this transaction now (even though
# create_and_send_nonmember_event also does it) because, if there's one,
# then we want to skip the call to redact_events_related_to.
event = None
if txn_id:
event = await self.event_creation_handler.get_event_from_transaction(
requester, txn_id, room_id
)

if event is None:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
requester,
{
"type": EventTypes.Redaction,
"content": content,
"room_id": room_id,
"sender": requester.user.to_string(),
"redacts": event_id,
},
txn_id=txn_id,
)

if with_relations:
run_in_background(
babolivier marked this conversation as resolved.
Show resolved Hide resolved
self._relation_handler.redact_events_related_to,
requester=requester,
event_id=event_id,
initial_redaction_event=event,
relation_types=with_relations,
)

event_id = event.event_id
except ShadowBanError:
event_id = "$" + random_string(43)
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
# Adds support for simple HTTP rendezvous as per MSC3886
"org.matrix.msc3886": self.config.experimental.msc3886_endpoint
is not None,
# Adds support for relation-based redactions as per MSC3912.
"org.matrix.msc3912": self.config.experimental.msc3912_enabled,
},
},
)
Expand Down
38 changes: 38 additions & 0 deletions synapse/storage/databases/main/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,44 @@ def _get_recent_references_for_event_txn(
"get_recent_references_for_event", _get_recent_references_for_event_txn
)

async def get_all_relations_for_event_with_types(
self,
event_id: str,
relation_types: List[str],
) -> List[str]:
"""Get the event IDs of all events that have a relation to the given event with
one of the given relation types.

Args:
event_id: The event for which to look for related events.
relation_types: The types of relations to look for.

Returns:
A list of the IDs of the events that relate to the given event with one of
the given relation types.
"""

def get_all_relation_ids_for_event_with_types_txn(
txn: LoggingTransaction,
) -> List[str]:
sql = """
SELECT event_id FROM event_relations
WHERE
relates_to_id = ?
AND relation_type IN (%s)
babolivier marked this conversation as resolved.
Show resolved Hide resolved
""" % (
",".join(["?" for _ in relation_types]),
)

txn.execute(sql, [event_id] + relation_types)

return [row[0] for row in txn]

return await self.db_pool.runInteraction(
desc="get_all_relation_ids_for_event_with_types",
func=get_all_relation_ids_for_event_with_types_txn,
)

async def event_includes_relation(self, event_id: str) -> bool:
"""Check if the given event relates to another event.

Expand Down
Loading