Skip to content

Commit 74ca7ae

Browse files
Add report user API from MSC4260 (#18120)
Co-authored-by: turt2live <[email protected]> Co-authored-by: Andrew Morgan <[email protected]>
1 parent 5102565 commit 74ca7ae

File tree

10 files changed

+335
-0
lines changed

10 files changed

+335
-0
lines changed

changelog.d/18120.feature

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

docs/usage/configuration/config_documentation.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,6 +1937,33 @@ rc_delayed_event_mgmt:
19371937
burst_count: 20.0
19381938
```
19391939
---
1940+
### `rc_reports`
1941+
1942+
*(object)* Ratelimiting settings for reporting content.
1943+
This is a ratelimiting option that ratelimits reports made by users about content they see.
1944+
Setting this to a high value allows users to report content quickly, possibly in duplicate. This can result in higher database usage.
1945+
1946+
This setting has the following sub-options:
1947+
1948+
* `per_second` (number): Maximum number of requests a client can send per second.
1949+
1950+
* `burst_count` (number): Maximum number of requests a client can send before being throttled.
1951+
1952+
Default configuration:
1953+
```yaml
1954+
rc_reports:
1955+
per_user:
1956+
per_second: 1.0
1957+
burst_count: 5.0
1958+
```
1959+
1960+
Example configuration:
1961+
```yaml
1962+
rc_reports:
1963+
per_second: 2.0
1964+
burst_count: 20.0
1965+
```
1966+
---
19401967
### `federation_rr_transactions_per_room_per_second`
19411968

19421969
*(integer)* Sets outgoing federation transaction frequency for sending read-receipts, per-room.

schema/synapse-config.schema.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,6 +2185,23 @@ properties:
21852185
examples:
21862186
- per_second: 2.0
21872187
burst_count: 20.0
2188+
rc_reports:
2189+
$ref: "#/$defs/rc"
2190+
description: >-
2191+
Ratelimiting settings for reporting content.
2192+
2193+
This is a ratelimiting option that ratelimits reports made by users
2194+
about content they see.
2195+
2196+
Setting this to a high value allows users to report content quickly, possibly in
2197+
duplicate. This can result in higher database usage.
2198+
default:
2199+
per_user:
2200+
per_second: 1.0
2201+
burst_count: 5.0
2202+
examples:
2203+
- per_second: 2.0
2204+
burst_count: 20.0
21882205
federation_rr_transactions_per_room_per_second:
21892206
type: integer
21902207
description: >-

synapse/config/ratelimiting.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,9 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
240240
"rc_delayed_event_mgmt",
241241
defaults={"per_second": 1, "burst_count": 5},
242242
)
243+
244+
self.rc_reports = RatelimitSettings.parse(
245+
config,
246+
"rc_reports",
247+
defaults={"per_second": 1, "burst_count": 5},
248+
)

synapse/handlers/reports.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#
2+
# This file is licensed under the Affero General Public License (AGPL) version 3.
3+
#
4+
# Copyright 2015, 2016 OpenMarket Ltd
5+
# Copyright (C) 2023 New Vector, Ltd
6+
#
7+
# This program is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Affero General Public License as
9+
# published by the Free Software Foundation, either version 3 of the
10+
# License, or (at your option) any later version.
11+
#
12+
# See the GNU Affero General Public License for more details:
13+
# <https://www.gnu.org/licenses/agpl-3.0.html>.
14+
#
15+
#
16+
import logging
17+
from http import HTTPStatus
18+
from typing import TYPE_CHECKING
19+
20+
from synapse.api.errors import Codes, SynapseError
21+
from synapse.api.ratelimiting import Ratelimiter
22+
from synapse.types import (
23+
Requester,
24+
)
25+
26+
if TYPE_CHECKING:
27+
from synapse.server import HomeServer
28+
29+
logger = logging.getLogger(__name__)
30+
31+
32+
class ReportsHandler:
33+
def __init__(self, hs: "HomeServer"):
34+
self._hs = hs
35+
self._store = hs.get_datastores().main
36+
self._clock = hs.get_clock()
37+
38+
# Ratelimiter for management of existing delayed events,
39+
# keyed by the requesting user ID.
40+
self._reports_ratelimiter = Ratelimiter(
41+
store=self._store,
42+
clock=self._clock,
43+
cfg=hs.config.ratelimiting.rc_reports,
44+
)
45+
46+
async def report_user(
47+
self, requester: Requester, target_user_id: str, reason: str
48+
) -> None:
49+
"""Files a report against a user from a user.
50+
51+
Rate and size limits are applied to the report. If the user being reported
52+
does not belong to this server, the report is ignored. This check is done
53+
after the limits to reduce DoS potential.
54+
55+
If the user being reported belongs to this server, but doesn't exist, we
56+
similarly ignore the report. The spec allows us to return an error if we
57+
want to, but we choose to hide that user's existence instead.
58+
59+
If the report is otherwise valid (for a user which exists on our server),
60+
we append it to the database for later processing.
61+
62+
Args:
63+
requester - The user filing the report.
64+
target_user_id - The user being reported.
65+
reason - The user-supplied reason the user is being reported.
66+
67+
Raises:
68+
SynapseError for BAD_REQUEST/BAD_JSON if the reason is too long.
69+
"""
70+
71+
await self._check_limits(requester)
72+
73+
if len(reason) > 1000:
74+
raise SynapseError(
75+
HTTPStatus.BAD_REQUEST,
76+
"Reason must be less than 1000 characters",
77+
Codes.BAD_JSON,
78+
)
79+
80+
if not self._hs.is_mine_id(target_user_id):
81+
return # hide that they're not ours/that we can't do anything about them
82+
83+
user = await self._store.get_user_by_id(target_user_id)
84+
if user is None:
85+
return # hide that they don't exist
86+
87+
await self._store.add_user_report(
88+
target_user_id=target_user_id,
89+
user_id=requester.user.to_string(),
90+
reason=reason,
91+
received_ts=self._clock.time_msec(),
92+
)
93+
94+
async def _check_limits(self, requester: Requester) -> None:
95+
await self._reports_ratelimiter.ratelimit(
96+
requester,
97+
requester.user.to_string(),
98+
)

synapse/rest/client/reporting.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,44 @@ async def on_POST(
150150
return 200, {}
151151

152152

153+
class ReportUserRestServlet(RestServlet):
154+
"""This endpoint lets clients report a user for abuse.
155+
156+
Introduced by MSC4260: https://github.com/matrix-org/matrix-spec-proposals/pull/4260
157+
"""
158+
159+
PATTERNS = list(
160+
client_patterns(
161+
"/users/(?P<target_user_id>[^/]*)/report$",
162+
releases=("v3",),
163+
unstable=False,
164+
v1=False,
165+
)
166+
)
167+
168+
def __init__(self, hs: "HomeServer"):
169+
super().__init__()
170+
self.hs = hs
171+
self.auth = hs.get_auth()
172+
self.clock = hs.get_clock()
173+
self.store = hs.get_datastores().main
174+
self.handler = hs.get_reports_handler()
175+
176+
class PostBody(RequestBodyModel):
177+
reason: StrictStr
178+
179+
async def on_POST(
180+
self, request: SynapseRequest, target_user_id: str
181+
) -> Tuple[int, JsonDict]:
182+
requester = await self.auth.get_user_by_req(request)
183+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
184+
185+
await self.handler.report_user(requester, target_user_id, body.reason)
186+
187+
return 200, {}
188+
189+
153190
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
154191
ReportEventRestServlet(hs).register(http_server)
155192
ReportRoomRestServlet(hs).register(http_server)
193+
ReportUserRestServlet(hs).register(http_server)

synapse/server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
from synapse.handlers.receipts import ReceiptsHandler
9595
from synapse.handlers.register import RegistrationHandler
9696
from synapse.handlers.relations import RelationsHandler
97+
from synapse.handlers.reports import ReportsHandler
9798
from synapse.handlers.room import (
9899
RoomContextHandler,
99100
RoomCreationHandler,
@@ -718,6 +719,10 @@ def get_federation_sender(self) -> AbstractFederationSender:
718719
def get_receipts_handler(self) -> ReceiptsHandler:
719720
return ReceiptsHandler(self)
720721

722+
@cache_in_self
723+
def get_reports_handler(self) -> ReportsHandler:
724+
return ReportsHandler(self)
725+
721726
@cache_in_self
722727
def get_read_marker_handler(self) -> ReadMarkerHandler:
723728
return ReadMarkerHandler(self)

synapse/storage/databases/main/room.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,7 @@ def __init__(
24212421

24222422
self._event_reports_id_gen = IdGenerator(db_conn, "event_reports", "id")
24232423
self._room_reports_id_gen = IdGenerator(db_conn, "room_reports", "id")
2424+
self._user_reports_id_gen = IdGenerator(db_conn, "user_reports", "id")
24242425

24252426
self._instance_name = hs.get_instance_name()
24262427

@@ -2662,6 +2663,37 @@ async def add_room_report(
26622663
)
26632664
return next_id
26642665

2666+
async def add_user_report(
2667+
self,
2668+
target_user_id: str,
2669+
user_id: str,
2670+
reason: str,
2671+
received_ts: int,
2672+
) -> int:
2673+
"""Add a user report
2674+
2675+
Args:
2676+
target_user_id: The user ID being reported.
2677+
user_id: User who reported the user.
2678+
reason: Description that the user specifies.
2679+
received_ts: Time when the user submitted the report (milliseconds).
2680+
Returns:
2681+
ID of the room report.
2682+
"""
2683+
next_id = self._user_reports_id_gen.get_next()
2684+
await self.db_pool.simple_insert(
2685+
table="user_reports",
2686+
values={
2687+
"id": next_id,
2688+
"received_ts": received_ts,
2689+
"target_user_id": target_user_id,
2690+
"user_id": user_id,
2691+
"reason": reason,
2692+
},
2693+
desc="add_user_report",
2694+
)
2695+
return next_id
2696+
26652697
async def clear_partial_state_room(self, room_id: str) -> Optional[int]:
26662698
"""Clears the partial state flag for a room.
26672699
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--
2+
-- This file is licensed under the Affero General Public License (AGPL) version 3.
3+
--
4+
-- Copyright (C) 2025 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 user_reports (
15+
id BIGINT NOT NULL PRIMARY KEY,
16+
received_ts BIGINT NOT NULL,
17+
target_user_id TEXT NOT NULL,
18+
user_id TEXT NOT NULL,
19+
reason TEXT NOT NULL
20+
);
21+
CREATE INDEX user_reports_target_user_id ON user_reports(target_user_id); -- for lookups
22+
CREATE INDEX user_reports_user_id ON user_reports(user_id); -- for lookups

0 commit comments

Comments
 (0)