Skip to content

Commit f1c4dfb

Browse files
authored
Add report room API (MSC4151) (#17270)
matrix-org/matrix-spec-proposals#4151 This is intended to be enabled by default for immediate use. When FCP is complete, the unstable endpoint will be dropped and stable endpoint supported instead - no backwards compatibility is expected for the unstable endpoint.
1 parent 0edf1ca commit f1c4dfb

File tree

9 files changed

+210
-8
lines changed

9 files changed

+210
-8
lines changed

changelog.d/17270.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for the unstable [MSC4151](https://github.com/matrix-org/matrix-spec-proposals/pull/4151) report room API.

synapse/config/experimental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
443443
self.msc3916_authenticated_media_enabled = experimental.get(
444444
"msc3916_authenticated_media_enabled", False
445445
)
446+
447+
# MSC4151: Report room API (Client-Server API)
448+
self.msc4151_enabled: bool = experimental.get("msc4151_enabled", False)

synapse/rest/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
register,
5454
relations,
5555
rendezvous,
56-
report_event,
56+
reporting,
5757
room,
5858
room_keys,
5959
room_upgrade_rest_servlet,
@@ -128,7 +128,7 @@ def register_servlets(client_resource: HttpServer, hs: "HomeServer") -> None:
128128
tags.register_servlets(hs, client_resource)
129129
account_data.register_servlets(hs, client_resource)
130130
if is_main_process:
131-
report_event.register_servlets(hs, client_resource)
131+
reporting.register_servlets(hs, client_resource)
132132
openid.register_servlets(hs, client_resource)
133133
notifications.register_servlets(hs, client_resource)
134134
devices.register_servlets(hs, client_resource)

synapse/rest/client/report_event.py renamed to synapse/rest/client/reporting.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,28 @@
2323
from http import HTTPStatus
2424
from typing import TYPE_CHECKING, Tuple
2525

26+
from synapse._pydantic_compat import HAS_PYDANTIC_V2
2627
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
2728
from synapse.http.server import HttpServer
28-
from synapse.http.servlet import RestServlet, parse_json_object_from_request
29+
from synapse.http.servlet import (
30+
RestServlet,
31+
parse_and_validate_json_object_from_request,
32+
parse_json_object_from_request,
33+
)
2934
from synapse.http.site import SynapseRequest
3035
from synapse.types import JsonDict
36+
from synapse.types.rest import RequestBodyModel
3137

3238
from ._base import client_patterns
3339

3440
if TYPE_CHECKING:
3541
from synapse.server import HomeServer
3642

43+
if TYPE_CHECKING or HAS_PYDANTIC_V2:
44+
from pydantic.v1 import StrictStr
45+
else:
46+
from pydantic import StrictStr
47+
3748
logger = logging.getLogger(__name__)
3849

3950

@@ -95,5 +106,49 @@ async def on_POST(
95106
return 200, {}
96107

97108

109+
class ReportRoomRestServlet(RestServlet):
110+
# https://github.com/matrix-org/matrix-spec-proposals/pull/4151
111+
PATTERNS = client_patterns(
112+
"/org.matrix.msc4151/rooms/(?P<room_id>[^/]*)/report$",
113+
releases=[],
114+
v1=False,
115+
unstable=True,
116+
)
117+
118+
def __init__(self, hs: "HomeServer"):
119+
super().__init__()
120+
self.hs = hs
121+
self.auth = hs.get_auth()
122+
self.clock = hs.get_clock()
123+
self.store = hs.get_datastores().main
124+
125+
class PostBody(RequestBodyModel):
126+
reason: StrictStr
127+
128+
async def on_POST(
129+
self, request: SynapseRequest, room_id: str
130+
) -> Tuple[int, JsonDict]:
131+
requester = await self.auth.get_user_by_req(request)
132+
user_id = requester.user.to_string()
133+
134+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
135+
136+
room = await self.store.get_room(room_id)
137+
if room is None:
138+
raise NotFoundError("Room does not exist")
139+
140+
await self.store.add_room_report(
141+
room_id=room_id,
142+
user_id=user_id,
143+
reason=body.reason,
144+
received_ts=self.clock.time_msec(),
145+
)
146+
147+
return 200, {}
148+
149+
98150
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
99151
ReportEventRestServlet(hs).register(http_server)
152+
153+
if hs.config.experimental.msc4151_enabled:
154+
ReportRoomRestServlet(hs).register(http_server)

synapse/rest/client/versions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
149149
is not None
150150
)
151151
),
152+
# MSC4151: Report room API (Client-Server API)
153+
"org.matrix.msc4151": self.config.experimental.msc4151_enabled,
152154
},
153155
},
154156
)

synapse/storage/databases/main/room.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2207,6 +2207,7 @@ def __init__(
22072207
super().__init__(database, db_conn, hs)
22082208

22092209
self._event_reports_id_gen = IdGenerator(db_conn, "event_reports", "id")
2210+
self._room_reports_id_gen = IdGenerator(db_conn, "room_reports", "id")
22102211

22112212
self._instance_name = hs.get_instance_name()
22122213

@@ -2416,6 +2417,37 @@ async def add_event_report(
24162417
)
24172418
return next_id
24182419

2420+
async def add_room_report(
2421+
self,
2422+
room_id: str,
2423+
user_id: str,
2424+
reason: str,
2425+
received_ts: int,
2426+
) -> int:
2427+
"""Add a room report
2428+
2429+
Args:
2430+
room_id: The room ID being reported.
2431+
user_id: User who reports the room.
2432+
reason: Description that the user specifies.
2433+
received_ts: Time when the user submitted the report (milliseconds).
2434+
Returns:
2435+
Id of the room report.
2436+
"""
2437+
next_id = self._room_reports_id_gen.get_next()
2438+
await self.db_pool.simple_insert(
2439+
table="room_reports",
2440+
values={
2441+
"id": next_id,
2442+
"received_ts": received_ts,
2443+
"room_id": room_id,
2444+
"user_id": user_id,
2445+
"reason": reason,
2446+
},
2447+
desc="add_room_report",
2448+
)
2449+
return next_id
2450+
24192451
async def block_room(self, room_id: str, user_id: str) -> None:
24202452
"""Marks the room as blocked.
24212453
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--
2+
-- This file is licensed under the Affero General Public License (AGPL) version 3.
3+
--
4+
-- Copyright (C) 2024 New Vector, Ltd
5+
--
6+
-- This program is free software: you can redistribute it and/or modify
7+
-- it under the terms of the GNU Affero General Public License as
8+
-- published by the Free Software Foundation, either version 3 of the
9+
-- License, or (at your option) any later version.
10+
--
11+
-- See the GNU Affero General Public License for more details:
12+
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
13+
14+
CREATE TABLE room_reports (
15+
id BIGINT NOT NULL PRIMARY KEY,
16+
received_ts BIGINT NOT NULL,
17+
room_id TEXT NOT NULL,
18+
user_id TEXT NOT NULL,
19+
reason TEXT NOT NULL
20+
);

tests/rest/admin/test_event_reports.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
import synapse.rest.admin
2626
from synapse.api.errors import Codes
27-
from synapse.rest.client import login, report_event, room
27+
from synapse.rest.client import login, reporting, room
2828
from synapse.server import HomeServer
2929
from synapse.types import JsonDict
3030
from synapse.util import Clock
@@ -37,7 +37,7 @@ class EventReportsTestCase(unittest.HomeserverTestCase):
3737
synapse.rest.admin.register_servlets,
3838
login.register_servlets,
3939
room.register_servlets,
40-
report_event.register_servlets,
40+
reporting.register_servlets,
4141
]
4242

4343
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
@@ -453,7 +453,7 @@ class EventReportDetailTestCase(unittest.HomeserverTestCase):
453453
synapse.rest.admin.register_servlets,
454454
login.register_servlets,
455455
room.register_servlets,
456-
report_event.register_servlets,
456+
reporting.register_servlets,
457457
]
458458

459459
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:

tests/rest/client/test_report_event.py renamed to tests/rest/client/test_reporting.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from twisted.test.proto_helpers import MemoryReactor
2323

2424
import synapse.rest.admin
25-
from synapse.rest.client import login, report_event, room
25+
from synapse.rest.client import login, reporting, room
2626
from synapse.server import HomeServer
2727
from synapse.types import JsonDict
2828
from synapse.util import Clock
@@ -35,7 +35,7 @@ class ReportEventTestCase(unittest.HomeserverTestCase):
3535
synapse.rest.admin.register_servlets,
3636
login.register_servlets,
3737
room.register_servlets,
38-
report_event.register_servlets,
38+
reporting.register_servlets,
3939
]
4040

4141
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
@@ -139,3 +139,92 @@ def _assert_status(self, response_status: int, data: JsonDict) -> None:
139139
"POST", self.report_path, data, access_token=self.other_user_tok
140140
)
141141
self.assertEqual(response_status, channel.code, msg=channel.result["body"])
142+
143+
144+
class ReportRoomTestCase(unittest.HomeserverTestCase):
145+
servlets = [
146+
synapse.rest.admin.register_servlets,
147+
login.register_servlets,
148+
room.register_servlets,
149+
reporting.register_servlets,
150+
]
151+
152+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
153+
self.other_user = self.register_user("user", "pass")
154+
self.other_user_tok = self.login("user", "pass")
155+
156+
self.room_id = self.helper.create_room_as(
157+
self.other_user, tok=self.other_user_tok, is_public=True
158+
)
159+
self.report_path = (
160+
f"/_matrix/client/unstable/org.matrix.msc4151/rooms/{self.room_id}/report"
161+
)
162+
163+
@unittest.override_config(
164+
{
165+
"experimental_features": {"msc4151_enabled": True},
166+
}
167+
)
168+
def test_reason_str(self) -> None:
169+
data = {"reason": "this makes me sad"}
170+
self._assert_status(200, data)
171+
172+
@unittest.override_config(
173+
{
174+
"experimental_features": {"msc4151_enabled": True},
175+
}
176+
)
177+
def test_no_reason(self) -> None:
178+
data = {"not_reason": "for typechecking"}
179+
self._assert_status(400, data)
180+
181+
@unittest.override_config(
182+
{
183+
"experimental_features": {"msc4151_enabled": True},
184+
}
185+
)
186+
def test_reason_nonstring(self) -> None:
187+
data = {"reason": 42}
188+
self._assert_status(400, data)
189+
190+
@unittest.override_config(
191+
{
192+
"experimental_features": {"msc4151_enabled": True},
193+
}
194+
)
195+
def test_reason_null(self) -> None:
196+
data = {"reason": None}
197+
self._assert_status(400, data)
198+
199+
@unittest.override_config(
200+
{
201+
"experimental_features": {"msc4151_enabled": True},
202+
}
203+
)
204+
def test_cannot_report_nonexistent_room(self) -> None:
205+
"""
206+
Tests that we don't accept event reports for rooms which do not exist.
207+
"""
208+
channel = self.make_request(
209+
"POST",
210+
"/_matrix/client/unstable/org.matrix.msc4151/rooms/!bloop:example.org/report",
211+
{"reason": "i am very sad"},
212+
access_token=self.other_user_tok,
213+
shorthand=False,
214+
)
215+
self.assertEqual(404, channel.code, msg=channel.result["body"])
216+
self.assertEqual(
217+
"Room does not exist",
218+
channel.json_body["error"],
219+
msg=channel.result["body"],
220+
)
221+
222+
def _assert_status(self, response_status: int, data: JsonDict) -> None:
223+
channel = self.make_request(
224+
"POST",
225+
self.report_path,
226+
data,
227+
access_token=self.other_user_tok,
228+
shorthand=False,
229+
)
230+
self.assertEqual(response_status, channel.code, msg=channel.result["body"])

0 commit comments

Comments
 (0)