mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-01-03 20:30:49 -05:00
Merge pull request #9150 from Yoric/develop-context
New API /_synapse/admin/rooms/{roomId}/context/{eventId}
This commit is contained in:
commit
b0b2cac057
1
changelog.d/9150.feature
Normal file
1
changelog.d/9150.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
New API /_synapse/admin/rooms/{roomId}/context/{eventId}.
|
@ -10,6 +10,7 @@
|
|||||||
* [Undoing room shutdowns](#undoing-room-shutdowns)
|
* [Undoing room shutdowns](#undoing-room-shutdowns)
|
||||||
- [Make Room Admin API](#make-room-admin-api)
|
- [Make Room Admin API](#make-room-admin-api)
|
||||||
- [Forward Extremities Admin API](#forward-extremities-admin-api)
|
- [Forward Extremities Admin API](#forward-extremities-admin-api)
|
||||||
|
- [Event Context API](#event-context-api)
|
||||||
|
|
||||||
# List Room API
|
# List Room API
|
||||||
|
|
||||||
@ -594,3 +595,121 @@ that were deleted.
|
|||||||
"deleted": 1
|
"deleted": 1
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Event Context API
|
||||||
|
|
||||||
|
This API lets a client find the context of an event. This is designed primarily to investigate abuse reports.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /_synapse/admin/v1/rooms/<room_id>/context/<event_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
This API mimmicks [GET /_matrix/client/r0/rooms/{roomId}/context/{eventId}](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-context-eventid). Please refer to the link for all details on parameters and reseponse.
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"end": "t29-57_2_0_2",
|
||||||
|
"events_after": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "This is an example text message",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "<b>This is an example text message</b>"
|
||||||
|
},
|
||||||
|
"type": "m.room.message",
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"room_id": "!636q39766251:example.com",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": {
|
||||||
|
"content": {
|
||||||
|
"body": "filename.jpg",
|
||||||
|
"info": {
|
||||||
|
"h": 398,
|
||||||
|
"w": 394,
|
||||||
|
"mimetype": "image/jpeg",
|
||||||
|
"size": 31037
|
||||||
|
},
|
||||||
|
"url": "mxc://example.org/JWEIFJgwEIhweiWJE",
|
||||||
|
"msgtype": "m.image"
|
||||||
|
},
|
||||||
|
"type": "m.room.message",
|
||||||
|
"event_id": "$f3h4d129462ha:example.com",
|
||||||
|
"room_id": "!636q39766251:example.com",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"events_before": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "something-important.doc",
|
||||||
|
"filename": "something-important.doc",
|
||||||
|
"info": {
|
||||||
|
"mimetype": "application/msword",
|
||||||
|
"size": 46144
|
||||||
|
},
|
||||||
|
"msgtype": "m.file",
|
||||||
|
"url": "mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe"
|
||||||
|
},
|
||||||
|
"type": "m.room.message",
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"room_id": "!636q39766251:example.com",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start": "t27-54_2_0_2",
|
||||||
|
"state": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"creator": "@example:example.org",
|
||||||
|
"room_version": "1",
|
||||||
|
"m.federate": true,
|
||||||
|
"predecessor": {
|
||||||
|
"event_id": "$something:example.org",
|
||||||
|
"room_id": "!oldroom:example.org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.room.create",
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"room_id": "!636q39766251:example.com",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
},
|
||||||
|
"state_key": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"membership": "join",
|
||||||
|
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||||
|
"displayname": "Alice Margatroid"
|
||||||
|
},
|
||||||
|
"type": "m.room.member",
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"room_id": "!636q39766251:example.com",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
},
|
||||||
|
"state_key": "@alice:example.org"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -38,6 +38,7 @@ from synapse.api.filtering import Filter
|
|||||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
|
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.events.utils import copy_power_levels_contents
|
from synapse.events.utils import copy_power_levels_contents
|
||||||
|
from synapse.rest.admin._base import assert_user_is_admin
|
||||||
from synapse.storage.state import StateFilter
|
from synapse.storage.state import StateFilter
|
||||||
from synapse.types import (
|
from synapse.types import (
|
||||||
JsonDict,
|
JsonDict,
|
||||||
@ -1004,41 +1005,51 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
class RoomContextHandler:
|
class RoomContextHandler:
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
self.auth = hs.get_auth()
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.storage = hs.get_storage()
|
self.storage = hs.get_storage()
|
||||||
self.state_store = self.storage.state
|
self.state_store = self.storage.state
|
||||||
|
|
||||||
async def get_event_context(
|
async def get_event_context(
|
||||||
self,
|
self,
|
||||||
user: UserID,
|
requester: Requester,
|
||||||
room_id: str,
|
room_id: str,
|
||||||
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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user
|
requester
|
||||||
room_id
|
room_id
|
||||||
event_id
|
event_id
|
||||||
limit: The maximum number of events to return in total
|
limit: The maximum number of events to return in total
|
||||||
(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
|
||||||
"""
|
"""
|
||||||
|
user = requester.user
|
||||||
|
if use_admin_priviledge:
|
||||||
|
await assert_user_is_admin(self.auth, requester.user)
|
||||||
|
|
||||||
before_limit = math.floor(limit / 2.0)
|
before_limit = math.floor(limit / 2.0)
|
||||||
after_limit = limit - before_limit
|
after_limit = limit - before_limit
|
||||||
|
|
||||||
users = await self.store.get_users_in_room(room_id)
|
users = await self.store.get_users_in_room(room_id)
|
||||||
is_peeking = user.to_string() not in users
|
is_peeking = user.to_string() not in users
|
||||||
|
|
||||||
def filter_evts(events):
|
async def filter_evts(events):
|
||||||
return filter_events_for_client(
|
if use_admin_priviledge:
|
||||||
|
return events
|
||||||
|
return await filter_events_for_client(
|
||||||
self.storage, user.to_string(), events, is_peeking=is_peeking
|
self.storage, user.to_string(), events, is_peeking=is_peeking
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ from synapse.rest.admin.rooms import (
|
|||||||
JoinRoomAliasServlet,
|
JoinRoomAliasServlet,
|
||||||
ListRoomRestServlet,
|
ListRoomRestServlet,
|
||||||
MakeRoomAdminRestServlet,
|
MakeRoomAdminRestServlet,
|
||||||
|
RoomEventContextServlet,
|
||||||
RoomMembersRestServlet,
|
RoomMembersRestServlet,
|
||||||
RoomRestServlet,
|
RoomRestServlet,
|
||||||
RoomStateRestServlet,
|
RoomStateRestServlet,
|
||||||
@ -238,6 +239,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):
|
||||||
|
@ -15,9 +15,11 @@
|
|||||||
import logging
|
import logging
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
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.constants import EventTypes, JoinRules, Membership
|
||||||
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
|
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
|
||||||
|
from synapse.api.filtering import Filter
|
||||||
from synapse.http.servlet import (
|
from synapse.http.servlet import (
|
||||||
RestServlet,
|
RestServlet,
|
||||||
assert_params_in_dict,
|
assert_params_in_dict,
|
||||||
@ -33,6 +35,7 @@ from synapse.rest.admin._base import (
|
|||||||
)
|
)
|
||||||
from synapse.storage.databases.main.room import RoomSortOrder
|
from synapse.storage.databases.main.room import RoomSortOrder
|
||||||
from synapse.types import JsonDict, RoomAlias, RoomID, UserID, create_requester
|
from synapse.types import JsonDict, RoomAlias, RoomID, UserID, create_requester
|
||||||
|
from synapse.util import json_decoder
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
@ -605,3 +608,65 @@ 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):
|
||||||
|
"""
|
||||||
|
Provide the context for an event.
|
||||||
|
This API is designed to be used when system administrators wish to look at
|
||||||
|
an abuse report and understand what happened during and immediately prior
|
||||||
|
to this event.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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=False)
|
||||||
|
await assert_user_is_admin(self.auth, requester.user)
|
||||||
|
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
@ -650,7 +650,7 @@ class RoomEventContextServlet(RestServlet):
|
|||||||
event_filter = None
|
event_filter = None
|
||||||
|
|
||||||
results = await self.room_context_handler.get_event_context(
|
results = await self.room_context_handler.get_event_context(
|
||||||
requester.user, room_id, event_id, limit, event_filter
|
requester, room_id, event_id, limit, event_filter
|
||||||
)
|
)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
|
@ -80,6 +80,7 @@ async def filter_events_for_client(
|
|||||||
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 = ((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),
|
||||||
|
@ -1445,6 +1445,90 @@ 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_as_non_admin(self):
|
||||||
|
"""
|
||||||
|
Test that, without being admin, one cannot use the context admin API
|
||||||
|
"""
|
||||||
|
# Create a room.
|
||||||
|
user_id = self.register_user("test", "test")
|
||||||
|
user_tok = self.login("test", "test")
|
||||||
|
|
||||||
|
self.register_user("test_2", "test")
|
||||||
|
user_tok_2 = self.login("test_2", "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 attempt to find the context using the admin API without being admin.
|
||||||
|
midway = (len(events) - 1) // 2
|
||||||
|
for tok in [user_tok, user_tok_2]:
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
"/_synapse/admin/v1/rooms/%s/context/%s"
|
||||||
|
% (room_id, events[midway]["event_id"]),
|
||||||
|
access_token=tok,
|
||||||
|
)
|
||||||
|
self.assertEquals(
|
||||||
|
403, int(channel.result["code"]), msg=channel.result["body"]
|
||||||
|
)
|
||||||
|
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
def test_context_as_admin(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 New Issue
Block a user