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

Commit

Permalink
New API /_synapse/admin/rooms/{roomId}/context/{eventId}
Browse files Browse the repository at this point in the history
Signed-off-by: David Teller <davidt@element.io>
  • Loading branch information
Yoric committed Jan 18, 2021
1 parent 883d4e6 commit f1b8480
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 8 deletions.
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

0 comments on commit f1b8480

Please sign in to comment.