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

New API /_synapse/admin/rooms/{roomId}/context/{eventId} #9149

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.d/9149.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
New API /_synapse/admin/rooms/{roomId}/context/{eventId}

This API mirrors /_matrix/client/r0/rooms/{roomId}/context/{eventId} but lets administrators
inspect rooms. Designed to annotate abuse reports with context.
17 changes: 17 additions & 0 deletions docs/admin_api/rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,3 +511,20 @@ optionally be specified, e.g.:
"user_id": "@foo:example.com"
}
```

# Event context API

Fetch context around an event for a room.

```
POST /_synapse/admin/rooms/<room_id>/context/<event_id>
{
"event": // details about `event_id`
"events_before": [] // details about the events just before `event_id` in this room
"events_after": [] // detailsa bout the events just after `event_id` in this room
}
```

This API replicates the behavior of `/_matrix/client/r0/rooms/{roomId}/context/{eventId}`, including filtering, paginating, etc.

See https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-context-eventid for more details about this API.
11 changes: 9 additions & 2 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@ async def get_event_context(
event_id: str,
limit: int,
event_filter: Optional[Filter],
use_admin_priviledge: bool = False,
) -> Optional[JsonDict]:
"""Retrieves events, pagination tokens and state around a given event
in a room.
Expand All @@ -1020,7 +1021,9 @@ async def get_event_context(
(excluding state).
event_filter: the filter to apply to the events returned
(excluding the target event_id)

use_admin_priviledge: if `True`, return all events, regardless
of whether `user` has access to them. To be used **ONLY**
from the admin API.
Returns:
dict, or None if the event isn't found
"""
Expand All @@ -1032,7 +1035,11 @@ async def get_event_context(

def filter_evts(events):
return filter_events_for_client(
self.storage, user.to_string(), events, is_peeking=is_peeking
self.storage,
user.to_string(),
events,
is_peeking=is_peeking,
use_admin_priviledge=use_admin_priviledge,
)

event = await self.store.get_event(
Expand Down
57 changes: 57 additions & 0 deletions synapse/rest/admin/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import logging
from http import HTTPStatus
from typing import TYPE_CHECKING, List, Optional, Tuple
from urllib import parse as urlparse

from synapse.api.constants import EventTypes, JoinRules, Membership
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
from synapse.api.filtering import Filter
from synapse.http.servlet import (
RestServlet,
assert_params_in_dict,
Expand All @@ -33,6 +35,7 @@
)
from synapse.storage.databases.main.room import RoomSortOrder
from synapse.types import JsonDict, RoomAlias, RoomID, UserID, create_requester
from synapse.util import json_decoder

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -499,3 +502,57 @@ async def on_POST(self, request, room_identifier):
)

return 200, {}


class RoomEventContextServlet(RestServlet):
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$")

def __init__(self, hs):
super().__init__()
self.clock = hs.get_clock()
self.room_context_handler = hs.get_room_context_handler()
self._event_serializer = hs.get_event_client_serializer()
self.auth = hs.get_auth()

async def on_GET(self, request, room_id, event_id):
requester = await self.auth.get_user_by_req(request, allow_guest=True)

limit = parse_integer(request, "limit", default=10)

# picking the API shape for symmetry with /messages
filter_str = parse_string(request, b"filter", encoding="utf-8")
if filter_str:
filter_json = urlparse.unquote(filter_str)
event_filter = Filter(
json_decoder.decode(filter_json)
) # type: Optional[Filter]
else:
event_filter = None

results = await self.room_context_handler.get_event_context(
requester.user,
room_id,
event_id,
limit,
event_filter,
use_admin_priviledge=True,
)

if not results:
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)

time_now = self.clock.time_msec()
results["events_before"] = await self._event_serializer.serialize_events(
results["events_before"], time_now
)
results["event"] = await self._event_serializer.serialize_event(
results["event"], time_now
)
results["events_after"] = await self._event_serializer.serialize_events(
results["events_after"], time_now
)
results["state"] = await self._event_serializer.serialize_events(
results["state"], time_now
)

return 200, results
26 changes: 20 additions & 6 deletions synapse/visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def filter_events_for_client(
is_peeking=False,
always_include_ids=frozenset(),
filter_send_to_client=True,
use_admin_priviledge=False,
):
"""
Check which events a user is allowed to see. If the user can see the event but its
Expand All @@ -71,6 +72,9 @@ async def filter_events_for_client(
filter_send_to_client (bool): Whether we're checking an event that's going to be
sent to a client. This might not always be the case since this function can
also be called to check whether a user can see the state at a given point.
use_admin_priviledge: if `True`, return all events, regardless
of whether `user` has access to them. To be used **ONLY**
from the admin API.

Returns:
list[synapse.events.EventBase]
Expand All @@ -79,15 +83,23 @@ async def filter_events_for_client(
# to clients.
events = [e for e in events if not e.internal_metadata.is_soft_failed()]

types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, user_id))
types = None
if use_admin_priviledge:
# Administrators can access all events.
types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, None))
else:
types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, user_id))

event_id_to_state = await storage.state.get_state_for_events(
frozenset(e.event_id for e in events),
state_filter=StateFilter.from_types(types),
)

ignore_dict_content = await storage.main.get_global_account_data_by_type_for_user(
AccountDataTypes.IGNORED_USER_LIST, user_id
)
ignore_dict_content = None
if not use_admin_priviledge:
ignore_dict_content = await storage.main.get_global_account_data_by_type_for_user(
AccountDataTypes.IGNORED_USER_LIST, user_id
)

ignore_list = frozenset()
if ignore_dict_content:
Expand Down Expand Up @@ -183,10 +195,12 @@ def allowed(event):
if old_priority < new_priority:
visibility = prev_visibility

membership = None
if use_admin_priviledge:
membership = Membership.JOIN
# likewise, if the event is the user's own membership event, use
# the 'most joined' membership
membership = None
if event.type == EventTypes.Member and event.state_key == user_id:
elif event.type == EventTypes.Member and event.state_key == user_id:
membership = event.content.get("membership", None)
if membership not in MEMBERSHIP_PRIORITY:
membership = "leave"
Expand Down
47 changes: 47 additions & 0 deletions tests/rest/admin/test_room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,53 @@ def test_join_private_room_if_owner(self):
self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0])

def test_context(self):
"""
Test that, as admin, we can find the context of an event without having joined the room.
"""

# Create a room. We're not part of it.
user_id = self.register_user("test", "test")
user_tok = self.login("test", "test")
room_id = self.helper.create_room_as(user_id, tok=user_tok)

# Populate the room with events.
events = []
for i in range(30):
events.append(
self.helper.send_event(
room_id, "com.example.test", content={"index": i}, tok=user_tok
)
)

# Now let's fetch the context for this room.
midway = (len(events) - 1) // 2
channel = self.make_request(
"GET",
"/_synapse/admin/rooms/%s/context/%s"
% (room_id, events[midway]["event_id"]),
)
self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEquals(
channel.result["body"]["event"]["event_id"], events[midway]["event_id"]
)

for i, found_event in channel.result["body"]["events_before"]:
for j, posted_event in enumerate(events):
if found_event["event_id"] == posted_event["event_id"]:
self.assertTrue(j < midway)
break
else:
self.fail("Event %s from events_before not found" % j)

for i, found_event in channel.result["body"]["events_after"]:
for j, posted_event in enumerate(events):
if found_event["event_id"] == posted_event["event_id"]:
self.assertTrue(j > midway)
break
else:
self.fail("Event %s from events_after not found" % j)


class MakeRoomAdminTestCase(unittest.HomeserverTestCase):
servlets = [
Expand Down