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

Commit 4bc8cb4

Browse files
authored
Implement MSC2815: allow room moderators to view redacted event content (#12427)
Implements matrix-org/matrix-spec-proposals#2815 Signed-off-by: Tulir Asokan <[email protected]>
1 parent eed38c5 commit 4bc8cb4

File tree

7 files changed

+100
-3
lines changed

7 files changed

+100
-3
lines changed

changelog.d/12427.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement [MSC2815](https://github.com/matrix-org/matrix-spec-proposals/pull/2815) to allow room moderators to view redacted event content. Contributed by @tulir.

synapse/api/errors.py

+18
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class Codes:
7979
UNABLE_AUTHORISE_JOIN = "M_UNABLE_TO_AUTHORISE_JOIN"
8080
UNABLE_TO_GRANT_JOIN = "M_UNABLE_TO_GRANT_JOIN"
8181

82+
UNREDACTED_CONTENT_DELETED = "FI.MAU.MSC2815_UNREDACTED_CONTENT_DELETED"
83+
8284

8385
class CodeMessageException(RuntimeError):
8486
"""An exception with integer code and message string attributes.
@@ -483,6 +485,22 @@ def __init__(self, inner_exception: BaseException, can_retry: bool):
483485
self.can_retry = can_retry
484486

485487

488+
class UnredactedContentDeletedError(SynapseError):
489+
def __init__(self, content_keep_ms: Optional[int] = None):
490+
super().__init__(
491+
404,
492+
"The content for that event has already been erased from the database",
493+
errcode=Codes.UNREDACTED_CONTENT_DELETED,
494+
)
495+
self.content_keep_ms = content_keep_ms
496+
497+
def error_dict(self) -> "JsonDict":
498+
extra = {}
499+
if self.content_keep_ms is not None:
500+
extra = {"fi.mau.msc2815.content_keep_ms": self.content_keep_ms}
501+
return cs_error(self.msg, self.errcode, **extra)
502+
503+
486504
def cs_error(msg: str, code: str = Codes.UNKNOWN, **kwargs: Any) -> "JsonDict":
487505
"""Utility method for constructing an error response for client-server
488506
interactions.

synapse/config/experimental.py

+3
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
7878

7979
# MSC2654: Unread counts
8080
self.msc2654_enabled: bool = experimental.get("msc2654_enabled", False)
81+
82+
# MSC2815 (allow room moderators to view redacted event content)
83+
self.msc2815_enabled: bool = experimental.get("msc2815_enabled", False)

synapse/handlers/events.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from synapse.events import EventBase
2222
from synapse.events.utils import SerializeEventConfig
2323
from synapse.handlers.presence import format_user_presence_state
24+
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
2425
from synapse.streams.config import PaginationConfig
2526
from synapse.types import JsonDict, UserID
2627
from synapse.visibility import filter_events_for_client
@@ -141,7 +142,11 @@ def __init__(self, hs: "HomeServer"):
141142
self.storage = hs.get_storage()
142143

143144
async def get_event(
144-
self, user: UserID, room_id: Optional[str], event_id: str
145+
self,
146+
user: UserID,
147+
room_id: Optional[str],
148+
event_id: str,
149+
show_redacted: bool = False,
145150
) -> Optional[EventBase]:
146151
"""Retrieve a single specified event.
147152
@@ -150,14 +155,20 @@ async def get_event(
150155
room_id: The expected room id. We'll return None if the
151156
event's room does not match.
152157
event_id: The event ID to obtain.
158+
show_redacted: Should the full content of redacted events be returned?
153159
Returns:
154160
An event, or None if there is no event matching this ID.
155161
Raises:
156162
SynapseError if there was a problem retrieving this event, or
157163
AuthError if the user does not have the rights to inspect this
158164
event.
159165
"""
160-
event = await self.store.get_event(event_id, check_room_id=room_id)
166+
redact_behaviour = (
167+
EventRedactBehaviour.AS_IS if show_redacted else EventRedactBehaviour.REDACT
168+
)
169+
event = await self.store.get_event(
170+
event_id, check_room_id=room_id, redact_behaviour=redact_behaviour
171+
)
161172

162173
if not event:
163174
return None

synapse/rest/client/room.py

+45-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from twisted.web.server import Request
2323

24+
from synapse import event_auth
2425
from synapse.api.constants import EventTypes, Membership
2526
from synapse.api.errors import (
2627
AuthError,
@@ -29,6 +30,7 @@
2930
MissingClientTokenError,
3031
ShadowBanError,
3132
SynapseError,
33+
UnredactedContentDeletedError,
3234
)
3335
from synapse.api.filtering import Filter
3436
from synapse.events.utils import format_event_for_client_v2
@@ -643,18 +645,55 @@ def __init__(self, hs: "HomeServer"):
643645
super().__init__()
644646
self.clock = hs.get_clock()
645647
self._store = hs.get_datastores().main
648+
self._state = hs.get_state_handler()
646649
self.event_handler = hs.get_event_handler()
647650
self._event_serializer = hs.get_event_client_serializer()
648651
self._relations_handler = hs.get_relations_handler()
649652
self.auth = hs.get_auth()
653+
self.content_keep_ms = hs.config.server.redaction_retention_period
654+
self.msc2815_enabled = hs.config.experimental.msc2815_enabled
650655

651656
async def on_GET(
652657
self, request: SynapseRequest, room_id: str, event_id: str
653658
) -> Tuple[int, JsonDict]:
654659
requester = await self.auth.get_user_by_req(request, allow_guest=True)
660+
661+
include_unredacted_content = self.msc2815_enabled and (
662+
parse_string(
663+
request,
664+
"fi.mau.msc2815.include_unredacted_content",
665+
allowed_values=("true", "false"),
666+
)
667+
== "true"
668+
)
669+
if include_unredacted_content and not await self.auth.is_server_admin(
670+
requester.user
671+
):
672+
power_level_event = await self._state.get_current_state(
673+
room_id, EventTypes.PowerLevels, ""
674+
)
675+
676+
auth_events = {}
677+
if power_level_event:
678+
auth_events[(EventTypes.PowerLevels, "")] = power_level_event
679+
680+
redact_level = event_auth.get_named_level(auth_events, "redact", 50)
681+
user_level = event_auth.get_user_power_level(
682+
requester.user.to_string(), auth_events
683+
)
684+
if user_level < redact_level:
685+
raise SynapseError(
686+
403,
687+
"You don't have permission to view redacted events in this room.",
688+
errcode=Codes.FORBIDDEN,
689+
)
690+
655691
try:
656692
event = await self.event_handler.get_event(
657-
requester.user, room_id, event_id
693+
requester.user,
694+
room_id,
695+
event_id,
696+
show_redacted=include_unredacted_content,
658697
)
659698
except AuthError:
660699
# This endpoint is supposed to return a 404 when the requester does
@@ -663,6 +702,11 @@ async def on_GET(
663702
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
664703

665704
if event:
705+
if include_unredacted_content and await self._store.have_censored_event(
706+
event_id
707+
):
708+
raise UnredactedContentDeletedError(self.content_keep_ms)
709+
666710
# Ensure there are bundled aggregations available.
667711
aggregations = await self._relations_handler.get_bundled_aggregations(
668712
[event], requester.user.to_string()

synapse/rest/client/versions.py

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
101101
"org.matrix.msc3030": self.config.experimental.msc3030_enabled,
102102
# Adds support for thread relations, per MSC3440.
103103
"org.matrix.msc3440.stable": True, # TODO: remove when "v1.3" is added above
104+
# Allows moderators to fetch redacted event content as described in MSC2815
105+
"fi.mau.msc2815": self.config.experimental.msc2815_enabled,
104106
},
105107
},
106108
)

synapse/storage/databases/main/events_worker.py

+18
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,24 @@ async def get_received_ts(self, event_id: str) -> Optional[int]:
303303
desc="get_received_ts",
304304
)
305305

306+
async def have_censored_event(self, event_id: str) -> bool:
307+
"""Check if an event has been censored, i.e. if the content of the event has been erased
308+
from the database due to a redaction.
309+
310+
Args:
311+
event_id: The event ID that was redacted.
312+
313+
Returns:
314+
True if the event has been censored, False otherwise.
315+
"""
316+
censored_redactions_list = await self.db_pool.simple_select_onecol(
317+
table="redactions",
318+
keyvalues={"redacts": event_id},
319+
retcol="have_censored",
320+
desc="get_have_censored",
321+
)
322+
return any(censored_redactions_list)
323+
306324
# Inform mypy that if allow_none is False (the default) then get_event
307325
# always returns an EventBase.
308326
@overload

0 commit comments

Comments
 (0)