Skip to content

Add report user API from MSC4260 #18120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f347547
Add report user API from MSC4260
turt2live Jan 30, 2025
b5f359a
changelog
turt2live Jan 30, 2025
f0dcc7a
Attempt to fix linting
turt2live Jan 30, 2025
61f2750
kick ci
turt2live Jan 30, 2025
facf07a
Include in /versions
turt2live Jan 31, 2025
85747b7
Annotate the tests instead
turt2live Jan 31, 2025
e8d102d
await
turt2live Jan 31, 2025
a842c66
Attempt to fix linting
turt2live Jan 31, 2025
44dbcab
kick ci
turt2live Jan 31, 2025
41d185c
Adjust testing
turt2live Jan 31, 2025
133380f
Attempt to fix linting
turt2live Jan 31, 2025
6ef7a87
kick ci
turt2live Jan 31, 2025
dbe43a1
Merge branch 'develop' into travis/report-user
turt2live Feb 13, 2025
8b84c23
Merge branch 'develop' into travis/report-user
turt2live Mar 18, 2025
79c2a0a
Move delta
turt2live Mar 18, 2025
1029a79
Unstable -> Stable
turt2live Mar 18, 2025
6ce8cc0
Attempt to fix linting
turt2live Mar 18, 2025
42b4207
Empty commit to fix CI
turt2live Mar 18, 2025
386a9e6
Apply suggestions from code review
turt2live May 2, 2025
2058b2b
Merge branch 'develop' into travis/report-user
turt2live May 2, 2025
c394ad1
Fix local user check
turt2live May 2, 2025
2f8958c
Limit length of `reason`; add rate limit; move to handler
turt2live May 2, 2025
9251b45
Attempt to fix linting
turt2live May 2, 2025
ff53217
Empty commit to kick CI
turt2live May 2, 2025
51aceab
move delta again
turt2live May 2, 2025
ba32d12
I guess imports are important
turt2live May 2, 2025
223df14
Add types
turt2live May 2, 2025
592a32d
Merge branch 'develop' into travis/report-user
turt2live Jun 19, 2025
cf03535
add docs
turt2live Jun 19, 2025
746c506
Add second index; move to new delta
turt2live Jun 19, 2025
7d46b07
move config
turt2live Jun 19, 2025
c4cdded
Use generated config
turt2live Jun 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/18120.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for the [MSC4260 user report API](https://github.com/matrix-org/matrix-spec-proposals/pull/4260).
27 changes: 27 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1937,6 +1937,33 @@ rc_delayed_event_mgmt:
burst_count: 20.0
```
---
### `rc_reports`

*(object)* Ratelimiting settings for reporting content.
This is a ratelimiting option that ratelimits reports made by users about content they see.
Setting this to a high value allows users to report content quickly, possibly in duplicate. This can result in higher database usage.

This setting has the following sub-options:

* `per_second` (number): Maximum number of requests a client can send per second.

* `burst_count` (number): Maximum number of requests a client can send before being throttled.

Default configuration:
```yaml
rc_reports:
per_user:
per_second: 1.0
burst_count: 5.0
```

Example configuration:
```yaml
rc_reports:
per_second: 2.0
burst_count: 20.0
```
---
### `federation_rr_transactions_per_room_per_second`

*(integer)* Sets outgoing federation transaction frequency for sending read-receipts, per-room.
Expand Down
17 changes: 17 additions & 0 deletions schema/synapse-config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,23 @@ properties:
examples:
- per_second: 2.0
burst_count: 20.0
rc_reports:
$ref: "#/$defs/rc"
description: >-
Ratelimiting settings for reporting content.

This is a ratelimiting option that ratelimits reports made by users
about content they see.

Setting this to a high value allows users to report content quickly, possibly in
duplicate. This can result in higher database usage.
default:
per_user:
per_second: 1.0
burst_count: 5.0
examples:
- per_second: 2.0
burst_count: 20.0
federation_rr_transactions_per_room_per_second:
type: integer
description: >-
Expand Down
6 changes: 6 additions & 0 deletions synapse/config/ratelimiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,9 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
"rc_delayed_event_mgmt",
defaults={"per_second": 1, "burst_count": 5},
)

self.rc_reports = RatelimitSettings.parse(
config,
"rc_reports",
defaults={"per_second": 1, "burst_count": 5},
)
98 changes: 98 additions & 0 deletions synapse/handlers/reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright (C) 2023 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
#
import logging
from http import HTTPStatus
from typing import TYPE_CHECKING

from synapse.api.errors import Codes, SynapseError
from synapse.api.ratelimiting import Ratelimiter
from synapse.types import (
Requester,
)

if TYPE_CHECKING:
from synapse.server import HomeServer

logger = logging.getLogger(__name__)


class ReportsHandler:
def __init__(self, hs: "HomeServer"):
self._hs = hs
self._store = hs.get_datastores().main
self._clock = hs.get_clock()

# Ratelimiter for management of existing delayed events,
# keyed by the requesting user ID.
self._reports_ratelimiter = Ratelimiter(
store=self._store,
clock=self._clock,
cfg=hs.config.ratelimiting.rc_reports,
)

async def report_user(
self, requester: Requester, target_user_id: str, reason: str
) -> None:
"""Files a report against a user from a user.

Rate and size limits are applied to the report. If the user being reported
does not belong to this server, the report is ignored. This check is done
after the limits to reduce DoS potential.

If the user being reported belongs to this server, but doesn't exist, we
similarly ignore the report. The spec allows us to return an error if we
want to, but we choose to hide that user's existence instead.

If the report is otherwise valid (for a user which exists on our server),
we append it to the database for later processing.

Args:
requester - The user filing the report.
target_user_id - The user being reported.
reason - The user-supplied reason the user is being reported.

Raises:
SynapseError for BAD_REQUEST/BAD_JSON if the reason is too long.
"""

await self._check_limits(requester)

if len(reason) > 1000:
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"Reason must be less than 1000 characters",
Codes.BAD_JSON,
)

if not self._hs.is_mine_id(target_user_id):
return # hide that they're not ours/that we can't do anything about them
Comment on lines +80 to +81
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, but would it not be useful for the reporter to get an error here telling them that the homeserver operator likely don't do much about remote users? And tell them to contact the remote homeserver's operator instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly/probably, though IIRC this is in the MSC.

One day we do also hope to support federated reports.


user = await self._store.get_user_by_id(target_user_id)
if user is None:
return # hide that they don't exist

await self._store.add_user_report(
target_user_id=target_user_id,
user_id=requester.user.to_string(),
reason=reason,
received_ts=self._clock.time_msec(),
)

async def _check_limits(self, requester: Requester) -> None:
await self._reports_ratelimiter.ratelimit(
requester,
requester.user.to_string(),
)
38 changes: 38 additions & 0 deletions synapse/rest/client/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,44 @@ async def on_POST(
return 200, {}


class ReportUserRestServlet(RestServlet):
"""This endpoint lets clients report a user for abuse.

Introduced by MSC4260: https://github.com/matrix-org/matrix-spec-proposals/pull/4260
"""

PATTERNS = list(
client_patterns(
"/users/(?P<target_user_id>[^/]*)/report$",
releases=("v3",),
unstable=False,
v1=False,
)
)

def __init__(self, hs: "HomeServer"):
super().__init__()
self.hs = hs
self.auth = hs.get_auth()
self.clock = hs.get_clock()
self.store = hs.get_datastores().main
self.handler = hs.get_reports_handler()

class PostBody(RequestBodyModel):
reason: StrictStr

async def on_POST(
self, request: SynapseRequest, target_user_id: str
) -> Tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request)
body = parse_and_validate_json_object_from_request(request, self.PostBody)

await self.handler.report_user(requester, target_user_id, body.reason)

return 200, {}


def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
ReportEventRestServlet(hs).register(http_server)
ReportRoomRestServlet(hs).register(http_server)
ReportUserRestServlet(hs).register(http_server)
5 changes: 5 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
from synapse.handlers.receipts import ReceiptsHandler
from synapse.handlers.register import RegistrationHandler
from synapse.handlers.relations import RelationsHandler
from synapse.handlers.reports import ReportsHandler
from synapse.handlers.room import (
RoomContextHandler,
RoomCreationHandler,
Expand Down Expand Up @@ -718,6 +719,10 @@ def get_federation_sender(self) -> AbstractFederationSender:
def get_receipts_handler(self) -> ReceiptsHandler:
return ReceiptsHandler(self)

@cache_in_self
def get_reports_handler(self) -> ReportsHandler:
return ReportsHandler(self)

@cache_in_self
def get_read_marker_handler(self) -> ReadMarkerHandler:
return ReadMarkerHandler(self)
Expand Down
32 changes: 32 additions & 0 deletions synapse/storage/databases/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -2421,6 +2421,7 @@ def __init__(

self._event_reports_id_gen = IdGenerator(db_conn, "event_reports", "id")
self._room_reports_id_gen = IdGenerator(db_conn, "room_reports", "id")
self._user_reports_id_gen = IdGenerator(db_conn, "user_reports", "id")

self._instance_name = hs.get_instance_name()

Expand Down Expand Up @@ -2662,6 +2663,37 @@ async def add_room_report(
)
return next_id

async def add_user_report(
self,
target_user_id: str,
user_id: str,
reason: str,
received_ts: int,
) -> int:
"""Add a user report

Args:
target_user_id: The user ID being reported.
user_id: User who reported the user.
reason: Description that the user specifies.
received_ts: Time when the user submitted the report (milliseconds).
Returns:
ID of the room report.
"""
next_id = self._user_reports_id_gen.get_next()
await self.db_pool.simple_insert(
table="user_reports",
values={
"id": next_id,
"received_ts": received_ts,
"target_user_id": target_user_id,
"user_id": user_id,
"reason": reason,
},
desc="add_user_report",
)
return next_id

async def clear_partial_state_room(self, room_id: str) -> Optional[int]:
"""Clears the partial state flag for a room.

Expand Down
22 changes: 22 additions & 0 deletions synapse/storage/schema/main/delta/92/07_add_user_reports.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2025 New Vector, Ltd
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- See the GNU Affero General Public License for more details:
-- <https://www.gnu.org/licenses/agpl-3.0.html>.

CREATE TABLE user_reports (
id BIGINT NOT NULL PRIMARY KEY,
received_ts BIGINT NOT NULL,
target_user_id TEXT NOT NULL,
user_id TEXT NOT NULL,
reason TEXT NOT NULL
);
CREATE INDEX user_reports_target_user_id ON user_reports(target_user_id); -- for lookups
CREATE INDEX user_reports_user_id ON user_reports(user_id); -- for lookups
Loading
Loading