forked from MirrorHub/synapse
New API /_synapse/admin/rooms/{roomId}/context/{eventId}
Signed-off-by: David Teller <davidt@element.io>
This commit is contained in:
parent
34efb4c604
commit
10332c175c
6 changed files with 136 additions and 8 deletions
4
changelog.d/9150.feature
Normal file
4
changelog.d/9150.feature
Normal file
|
@ -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.
|
|
@ -1008,6 +1008,7 @@ class RoomContextHandler:
|
||||||
event_id: str,
|
event_id: str,
|
||||||
limit: int,
|
limit: int,
|
||||||
event_filter: Optional[Filter],
|
event_filter: Optional[Filter],
|
||||||
|
use_admin_priviledge: bool = False,
|
||||||
) -> Optional[JsonDict]:
|
) -> Optional[JsonDict]:
|
||||||
"""Retrieves events, pagination tokens and state around a given event
|
"""Retrieves events, pagination tokens and state around a given event
|
||||||
in a room.
|
in a room.
|
||||||
|
@ -1020,7 +1021,9 @@ class RoomContextHandler:
|
||||||
(excluding state).
|
(excluding state).
|
||||||
event_filter: the filter to apply to the events returned
|
event_filter: the filter to apply to the events returned
|
||||||
(excluding the target event_id)
|
(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:
|
Returns:
|
||||||
dict, or None if the event isn't found
|
dict, or None if the event isn't found
|
||||||
"""
|
"""
|
||||||
|
@ -1032,7 +1035,11 @@ class RoomContextHandler:
|
||||||
|
|
||||||
def filter_evts(events):
|
def filter_evts(events):
|
||||||
return filter_events_for_client(
|
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(
|
event = await self.store.get_event(
|
||||||
|
|
|
@ -42,6 +42,7 @@ from synapse.rest.admin.rooms import (
|
||||||
JoinRoomAliasServlet,
|
JoinRoomAliasServlet,
|
||||||
ListRoomRestServlet,
|
ListRoomRestServlet,
|
||||||
MakeRoomAdminRestServlet,
|
MakeRoomAdminRestServlet,
|
||||||
|
RoomEventContextServlet,
|
||||||
RoomMembersRestServlet,
|
RoomMembersRestServlet,
|
||||||
RoomRestServlet,
|
RoomRestServlet,
|
||||||
ShutdownRoomRestServlet,
|
ShutdownRoomRestServlet,
|
||||||
|
@ -236,6 +237,7 @@ def register_servlets(hs, http_server):
|
||||||
MakeRoomAdminRestServlet(hs).register(http_server)
|
MakeRoomAdminRestServlet(hs).register(http_server)
|
||||||
ShadowBanRestServlet(hs).register(http_server)
|
ShadowBanRestServlet(hs).register(http_server)
|
||||||
ForwardExtremitiesRestServlet(hs).register(http_server)
|
ForwardExtremitiesRestServlet(hs).register(http_server)
|
||||||
|
RoomEventContextServlet(hs).register(http_server)
|
||||||
|
|
||||||
|
|
||||||
def register_servlets_for_client_rest_resource(hs, http_server):
|
def register_servlets_for_client_rest_resource(hs, http_server):
|
||||||
|
|
|
@ -566,3 +566,56 @@ class ForwardExtremitiesRestServlet(RestServlet):
|
||||||
|
|
||||||
extremities = await self.store.get_forward_extremities_for_room(room_id)
|
extremities = await self.store.get_forward_extremities_for_room(room_id)
|
||||||
return 200, {"count": len(extremities), "results": extremities}
|
return 200, {"count": len(extremities), "results": extremities}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -53,6 +53,7 @@ async def filter_events_for_client(
|
||||||
is_peeking=False,
|
is_peeking=False,
|
||||||
always_include_ids=frozenset(),
|
always_include_ids=frozenset(),
|
||||||
filter_send_to_client=True,
|
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
|
Check which events a user is allowed to see. If the user can see the event but its
|
||||||
|
@ -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
|
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
|
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.
|
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:
|
Returns:
|
||||||
list[synapse.events.EventBase]
|
list[synapse.events.EventBase]
|
||||||
|
@ -79,12 +83,20 @@ async def filter_events_for_client(
|
||||||
# to clients.
|
# to clients.
|
||||||
events = [e for e in events if not e.internal_metadata.is_soft_failed()]
|
events = [e for e in events if not e.internal_metadata.is_soft_failed()]
|
||||||
|
|
||||||
|
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))
|
types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, user_id))
|
||||||
|
|
||||||
event_id_to_state = await storage.state.get_state_for_events(
|
event_id_to_state = await storage.state.get_state_for_events(
|
||||||
frozenset(e.event_id for e in events),
|
frozenset(e.event_id for e in events),
|
||||||
state_filter=StateFilter.from_types(types),
|
state_filter=StateFilter.from_types(types),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ignore_dict_content = None
|
||||||
|
if not use_admin_priviledge:
|
||||||
ignore_dict_content = await storage.main.get_global_account_data_by_type_for_user(
|
ignore_dict_content = await storage.main.get_global_account_data_by_type_for_user(
|
||||||
AccountDataTypes.IGNORED_USER_LIST, user_id
|
AccountDataTypes.IGNORED_USER_LIST, user_id
|
||||||
)
|
)
|
||||||
|
@ -183,10 +195,12 @@ async def filter_events_for_client(
|
||||||
if old_priority < new_priority:
|
if old_priority < new_priority:
|
||||||
visibility = prev_visibility
|
visibility = prev_visibility
|
||||||
|
|
||||||
|
membership = None
|
||||||
|
if use_admin_priviledge:
|
||||||
|
membership = Membership.JOIN
|
||||||
# likewise, if the event is the user's own membership event, use
|
# likewise, if the event is the user's own membership event, use
|
||||||
# the 'most joined' membership
|
# the 'most joined' membership
|
||||||
membership = None
|
elif event.type == EventTypes.Member and event.state_key == user_id:
|
||||||
if event.type == EventTypes.Member and event.state_key == user_id:
|
|
||||||
membership = event.content.get("membership", None)
|
membership = event.content.get("membership", None)
|
||||||
if membership not in MEMBERSHIP_PRIORITY:
|
if membership not in MEMBERSHIP_PRIORITY:
|
||||||
membership = "leave"
|
membership = "leave"
|
||||||
|
|
|
@ -1430,6 +1430,54 @@ class JoinAliasRoomTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0])
|
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/v1/rooms/%s/context/%s"
|
||||||
|
% (room_id, events[midway]["event_id"]),
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEquals(
|
||||||
|
channel.json_body["event"]["event_id"], events[midway]["event_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, found_event in enumerate(channel.json_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 enumerate(channel.json_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):
|
class MakeRoomAdminTestCase(unittest.HomeserverTestCase):
|
||||||
servlets = [
|
servlets = [
|
||||||
|
|
Loading…
Reference in a new issue