From f347547d32669c285f06fcde74cab28d69fe23e9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Jan 2025 16:05:31 -0700 Subject: [PATCH 01/28] Add report user API from MSC4260 --- synapse/config/experimental.py | 3 + synapse/rest/client/reporting.py | 56 +++++++++++++ synapse/storage/databases/main/room.py | 40 ++++++++++ .../main/delta/88/07_add_user_reports.sql | 21 +++++ tests/rest/client/test_reporting.py | 78 +++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 synapse/storage/schema/main/delta/88/07_add_user_reports.sql diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 94a25c7ee83..2a482d6360a 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -447,3 +447,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None: # MSC4076: Add `disable_badge_count`` to pusher configuration self.msc4076_enabled: bool = experimental.get("msc4076_enabled", False) + + # MSC4260: Report user API (Client-Server) + self.msc4260_enabled: bool = experimental.get("msc4260_enabled", False) diff --git a/synapse/rest/client/reporting.py b/synapse/rest/client/reporting.py index c5037be8b75..9428fc093e5 100644 --- a/synapse/rest/client/reporting.py +++ b/synapse/rest/client/reporting.py @@ -150,6 +150,62 @@ 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 + """ + + # Cast the Iterable to a list so that we can `append` below. + PATTERNS = list( + client_patterns( + "/org.matrix.msc4260/users/(?P[^/]*)/report$", + releases=[], # unstable only + unstable=True, + 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 + + 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) + user_id = requester.user.to_string() + + body = parse_and_validate_json_object_from_request(request, self.PostBody) + + # We can't deal with non-local users. + if not self.hs.is_mine_id(target_user_id): + raise NotFoundError("User does not belong to this server") + + user = await self.store.get_user_by_id(target_user_id) + if user is None: + # raise NotFoundError("User does not exist") + return 200, {} # hide existence + + await self.store.add_user_report( + target_user_id=target_user_id, + user_id=user_id, + reason=body.reason, + received_ts=self.clock.time_msec(), + ) + + return 200, {} + + def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: ReportEventRestServlet(hs).register(http_server) ReportRoomRestServlet(hs).register(http_server) + + if hs.config.experimental.msc4260_enabled: + ReportUserRestServlet(hs).register(http_server) diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index d673adba164..794798b4be8 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -2303,6 +2303,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() @@ -2544,6 +2545,45 @@ 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 get_user_report_ids(self, target_user_id: str) -> List[str]: + return await self.db_pool.simple_select_onecol( + table="user_reports", + keyvalues={"target_user_id": target_user_id}, + retcol="id", + desc="get_user_report_ids", + ) + async def clear_partial_state_room(self, room_id: str) -> Optional[int]: """Clears the partial state flag for a room. diff --git a/synapse/storage/schema/main/delta/88/07_add_user_reports.sql b/synapse/storage/schema/main/delta/88/07_add_user_reports.sql new file mode 100644 index 00000000000..2521aefc51c --- /dev/null +++ b/synapse/storage/schema/main/delta/88/07_add_user_reports.sql @@ -0,0 +1,21 @@ +-- +-- 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: +-- . + +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 diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 723553979f7..84206680db0 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -28,6 +28,7 @@ from synapse.util import Clock from tests import unittest +from tests.unittest import override_config class ReportEventTestCase(unittest.HomeserverTestCase): @@ -201,3 +202,80 @@ def _assert_status(self, response_status: int, data: JsonDict) -> None: shorthand=False, ) self.assertEqual(response_status, channel.code, msg=channel.result["body"]) + + +@override_config({"experimental_features": {"msc4260_enabled": True}}) +class ReportUserTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + reporting.register_servlets, + ] + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + self.other_user = self.register_user("user", "pass") + self.other_user_tok = self.login("user", "pass") + + self.target_user_id = self.register_user("target_user", "pass") + self.report_path = f"/_matrix/client/unstable/org.matrix.msc4260/users/{self.target_user_id}/report" + + def test_reason_str(self) -> None: + data = {"reason": "this makes me sad"} + self._assert_status(200, data) + self.assertEqual(1, self.hs.get_datastores().main.get_user_report_ids(self.target_user_id)) + + def test_no_reason(self) -> None: + data = {"not_reason": "for typechecking"} + self._assert_status(400, data) + + def test_reason_nonstring(self) -> None: + data = {"reason": 42} + self._assert_status(400, data) + + def test_reason_null(self) -> None: + data = {"reason": None} + self._assert_status(400, data) + + def test_cannot_report_nonlcoal_user(self) -> None: + """ + Tests that we don't accept event reports for users which aren't local users. + """ + channel = self.make_request( + "POST", + "/_matrix/client/unstable/org.matrix.msc4260/users/@bloop:example.org/report", + {"reason": "i am very sad"}, + access_token=self.other_user_tok, + shorthand=False, + ) + self.assertEqual(404, channel.code, msg=channel.result["body"]) + self.assertEqual( + "User does not belong to this server", + channel.json_body["error"], + msg=channel.result["body"], + ) + + def test_can_report_nonexistent_user(self) -> None: + """ + Tests that we ignore reports for nonexistent users. + """ + target_user_id = f"@bloop:{self.hs.hostname}" + channel = self.make_request( + "POST", + f"/_matrix/client/unstable/org.matrix.msc4260/users/{target_user_id}/report", + {"reason": "i am very sad"}, + access_token=self.other_user_tok, + shorthand=False, + ) + self.assertEqual(200, channel.code, msg=channel.result["body"]) + self.assertEqual(0, self.hs.get_datastores().main.get_user_report_ids(target_user_id)) + + def _assert_status(self, response_status: int, data: JsonDict) -> None: + channel = self.make_request( + "POST", + self.report_path, + data, + access_token=self.other_user_tok, + shorthand=False, + ) + self.assertEqual(response_status, channel.code, msg=channel.result["body"]) From b5f359aa0c1884403df7208adfabf061fccaa738 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Jan 2025 16:08:11 -0700 Subject: [PATCH 02/28] changelog --- changelog.d/18120.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/18120.feature diff --git a/changelog.d/18120.feature b/changelog.d/18120.feature new file mode 100644 index 00000000000..8441a0811a4 --- /dev/null +++ b/changelog.d/18120.feature @@ -0,0 +1 @@ +Add support for the unstable [MSC4260](https://github.com/matrix-org/matrix-spec-proposals/pull/4260) report user API. \ No newline at end of file From f0dcc7a802f729e2c01700370a1990d84d116e99 Mon Sep 17 00:00:00 2001 From: turt2live <1190097+turt2live@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:13:19 +0000 Subject: [PATCH 03/28] Attempt to fix linting --- tests/rest/client/test_reporting.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 84206680db0..f6ed28d76f5 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -223,7 +223,9 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: def test_reason_str(self) -> None: data = {"reason": "this makes me sad"} self._assert_status(200, data) - self.assertEqual(1, self.hs.get_datastores().main.get_user_report_ids(self.target_user_id)) + self.assertEqual( + 1, self.hs.get_datastores().main.get_user_report_ids(self.target_user_id) + ) def test_no_reason(self) -> None: data = {"not_reason": "for typechecking"} @@ -268,7 +270,9 @@ def test_can_report_nonexistent_user(self) -> None: shorthand=False, ) self.assertEqual(200, channel.code, msg=channel.result["body"]) - self.assertEqual(0, self.hs.get_datastores().main.get_user_report_ids(target_user_id)) + self.assertEqual( + 0, self.hs.get_datastores().main.get_user_report_ids(target_user_id) + ) def _assert_status(self, response_status: int, data: JsonDict) -> None: channel = self.make_request( From 61f2750c9b7e578fee9aae3b46c4a45638e55aa7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Jan 2025 16:16:25 -0700 Subject: [PATCH 04/28] kick ci From facf07a568113f6645d1bec47893a340fc70c8c7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Jan 2025 17:13:45 -0700 Subject: [PATCH 05/28] Include in /versions --- synapse/rest/client/versions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 266a0b835b9..caad93104e7 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -174,6 +174,8 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "org.matrix.simplified_msc3575": msc3575_enabled, # Arbitrary key-value profile fields. "uk.tcpip.msc4133": self.config.experimental.msc4133_enabled, + # MSC4260: Report users API (Client-Server) + "org.matrix.msc4260": self.config.experimental.msc4260_enabled, }, }, ) From 85747b70a7cec36c5f90c32eab3a8dfa5268adfe Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Jan 2025 17:13:56 -0700 Subject: [PATCH 06/28] Annotate the tests instead --- tests/rest/client/test_reporting.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index f6ed28d76f5..450453d4cbd 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -204,7 +204,6 @@ def _assert_status(self, response_status: int, data: JsonDict) -> None: self.assertEqual(response_status, channel.code, msg=channel.result["body"]) -@override_config({"experimental_features": {"msc4260_enabled": True}}) class ReportUserTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets, @@ -220,6 +219,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.target_user_id = self.register_user("target_user", "pass") self.report_path = f"/_matrix/client/unstable/org.matrix.msc4260/users/{self.target_user_id}/report" + @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_reason_str(self) -> None: data = {"reason": "this makes me sad"} self._assert_status(200, data) @@ -227,18 +227,22 @@ def test_reason_str(self) -> None: 1, self.hs.get_datastores().main.get_user_report_ids(self.target_user_id) ) + @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_no_reason(self) -> None: data = {"not_reason": "for typechecking"} self._assert_status(400, data) + @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_reason_nonstring(self) -> None: data = {"reason": 42} self._assert_status(400, data) + @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_reason_null(self) -> None: data = {"reason": None} self._assert_status(400, data) + @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_cannot_report_nonlcoal_user(self) -> None: """ Tests that we don't accept event reports for users which aren't local users. @@ -257,6 +261,7 @@ def test_cannot_report_nonlcoal_user(self) -> None: msg=channel.result["body"], ) + @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_can_report_nonexistent_user(self) -> None: """ Tests that we ignore reports for nonexistent users. From e8d102d8c5f7afa4734d139381d77cdd3e183f2d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Jan 2025 17:30:57 -0700 Subject: [PATCH 07/28] await --- tests/rest/client/test_reporting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 450453d4cbd..8934cbeff8a 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -220,11 +220,11 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.report_path = f"/_matrix/client/unstable/org.matrix.msc4260/users/{self.target_user_id}/report" @override_config({"experimental_features": {"msc4260_enabled": True}}) - def test_reason_str(self) -> None: + async def test_reason_str(self) -> None: data = {"reason": "this makes me sad"} self._assert_status(200, data) self.assertEqual( - 1, self.hs.get_datastores().main.get_user_report_ids(self.target_user_id) + 1, await self.hs.get_datastores().main.get_user_report_ids(self.target_user_id) ) @override_config({"experimental_features": {"msc4260_enabled": True}}) @@ -262,7 +262,7 @@ def test_cannot_report_nonlcoal_user(self) -> None: ) @override_config({"experimental_features": {"msc4260_enabled": True}}) - def test_can_report_nonexistent_user(self) -> None: + async def test_can_report_nonexistent_user(self) -> None: """ Tests that we ignore reports for nonexistent users. """ @@ -276,7 +276,7 @@ def test_can_report_nonexistent_user(self) -> None: ) self.assertEqual(200, channel.code, msg=channel.result["body"]) self.assertEqual( - 0, self.hs.get_datastores().main.get_user_report_ids(target_user_id) + 0, await self.hs.get_datastores().main.get_user_report_ids(target_user_id) ) def _assert_status(self, response_status: int, data: JsonDict) -> None: From a842c66ecb8dee8e711661e7ecbd9ae8512717cc Mon Sep 17 00:00:00 2001 From: turt2live <1190097+turt2live@users.noreply.github.com> Date: Fri, 31 Jan 2025 00:32:51 +0000 Subject: [PATCH 08/28] Attempt to fix linting --- tests/rest/client/test_reporting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 8934cbeff8a..57e2ff08273 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -224,7 +224,10 @@ async def test_reason_str(self) -> None: data = {"reason": "this makes me sad"} self._assert_status(200, data) self.assertEqual( - 1, await self.hs.get_datastores().main.get_user_report_ids(self.target_user_id) + 1, + await self.hs.get_datastores().main.get_user_report_ids( + self.target_user_id + ), ) @override_config({"experimental_features": {"msc4260_enabled": True}}) From 44dbcab047b82889c12328c7e99b22a9c4966aea Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 31 Jan 2025 11:29:14 -0700 Subject: [PATCH 09/28] kick ci From 41d185c71cf1da26d71847d00a4dfe873d7efe57 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 31 Jan 2025 12:02:12 -0700 Subject: [PATCH 10/28] Adjust testing --- synapse/storage/databases/main/room.py | 8 ------- tests/rest/client/test_reporting.py | 29 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 794798b4be8..d0c57b29d84 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -2576,14 +2576,6 @@ async def add_user_report( ) return next_id - async def get_user_report_ids(self, target_user_id: str) -> List[str]: - return await self.db_pool.simple_select_onecol( - table="user_reports", - keyvalues={"target_user_id": target_user_id}, - retcol="id", - desc="get_user_report_ids", - ) - async def clear_partial_state_room(self, room_id: str) -> Optional[int]: """Clears the partial state flag for a room. diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 57e2ff08273..2789a20e078 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -220,15 +220,17 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.report_path = f"/_matrix/client/unstable/org.matrix.msc4260/users/{self.target_user_id}/report" @override_config({"experimental_features": {"msc4260_enabled": True}}) - async def test_reason_str(self) -> None: + def test_reason_str(self) -> None: data = {"reason": "this makes me sad"} self._assert_status(200, data) - self.assertEqual( - 1, - await self.hs.get_datastores().main.get_user_report_ids( - self.target_user_id - ), - ) + + rows = self.get_success(self.hs.get_datastores().main.db_pool.simple_select_onecol( + table="user_reports", + keyvalues={"target_user_id": self.target_user_id}, + retcol="id", + desc="get_user_report_ids", + )) + self.assertEqual(len(rows), 1) @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_no_reason(self) -> None: @@ -265,7 +267,7 @@ def test_cannot_report_nonlcoal_user(self) -> None: ) @override_config({"experimental_features": {"msc4260_enabled": True}}) - async def test_can_report_nonexistent_user(self) -> None: + def test_can_report_nonexistent_user(self) -> None: """ Tests that we ignore reports for nonexistent users. """ @@ -278,9 +280,14 @@ async def test_can_report_nonexistent_user(self) -> None: shorthand=False, ) self.assertEqual(200, channel.code, msg=channel.result["body"]) - self.assertEqual( - 0, await self.hs.get_datastores().main.get_user_report_ids(target_user_id) - ) + + rows = self.get_success(self.hs.get_datastores().main.db_pool.simple_select_onecol( + table="user_reports", + keyvalues={"target_user_id": self.target_user_id}, + retcol="id", + desc="get_user_report_ids", + )) + self.assertEqual(len(rows), 0) def _assert_status(self, response_status: int, data: JsonDict) -> None: channel = self.make_request( From 133380ff6d916db50a513660af9e1b2f53b5ba0e Mon Sep 17 00:00:00 2001 From: turt2live <1190097+turt2live@users.noreply.github.com> Date: Fri, 31 Jan 2025 19:03:54 +0000 Subject: [PATCH 11/28] Attempt to fix linting --- tests/rest/client/test_reporting.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 2789a20e078..86ca1a3a166 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -224,12 +224,14 @@ def test_reason_str(self) -> None: data = {"reason": "this makes me sad"} self._assert_status(200, data) - rows = self.get_success(self.hs.get_datastores().main.db_pool.simple_select_onecol( - table="user_reports", - keyvalues={"target_user_id": self.target_user_id}, - retcol="id", - desc="get_user_report_ids", - )) + rows = self.get_success( + self.hs.get_datastores().main.db_pool.simple_select_onecol( + table="user_reports", + keyvalues={"target_user_id": self.target_user_id}, + retcol="id", + desc="get_user_report_ids", + ) + ) self.assertEqual(len(rows), 1) @override_config({"experimental_features": {"msc4260_enabled": True}}) @@ -281,12 +283,14 @@ def test_can_report_nonexistent_user(self) -> None: ) self.assertEqual(200, channel.code, msg=channel.result["body"]) - rows = self.get_success(self.hs.get_datastores().main.db_pool.simple_select_onecol( - table="user_reports", - keyvalues={"target_user_id": self.target_user_id}, - retcol="id", - desc="get_user_report_ids", - )) + rows = self.get_success( + self.hs.get_datastores().main.db_pool.simple_select_onecol( + table="user_reports", + keyvalues={"target_user_id": self.target_user_id}, + retcol="id", + desc="get_user_report_ids", + ) + ) self.assertEqual(len(rows), 0) def _assert_status(self, response_status: int, data: JsonDict) -> None: From 6ef7a87dc157f62921b470bcafc6971d24d4cf60 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 31 Jan 2025 12:04:03 -0700 Subject: [PATCH 12/28] kick ci From 79c2a0ac8d6645be2a759da380b16566bfe0e01a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Mar 2025 15:07:31 -0600 Subject: [PATCH 13/28] Move delta --- .../{88/07_add_user_reports.sql => 90/02_add_user_reports.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename synapse/storage/schema/main/delta/{88/07_add_user_reports.sql => 90/02_add_user_reports.sql} (100%) diff --git a/synapse/storage/schema/main/delta/88/07_add_user_reports.sql b/synapse/storage/schema/main/delta/90/02_add_user_reports.sql similarity index 100% rename from synapse/storage/schema/main/delta/88/07_add_user_reports.sql rename to synapse/storage/schema/main/delta/90/02_add_user_reports.sql From 1029a79ee666c24325596a8650a7cf24c4dbe814 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Mar 2025 15:10:08 -0600 Subject: [PATCH 14/28] Unstable -> Stable --- changelog.d/18120.feature | 2 +- synapse/config/experimental.py | 3 --- synapse/rest/client/reporting.py | 11 ++++------- synapse/rest/client/versions.py | 2 -- tests/rest/client/test_reporting.py | 12 +++--------- 5 files changed, 8 insertions(+), 22 deletions(-) diff --git a/changelog.d/18120.feature b/changelog.d/18120.feature index 8441a0811a4..15cfabba423 100644 --- a/changelog.d/18120.feature +++ b/changelog.d/18120.feature @@ -1 +1 @@ -Add support for the unstable [MSC4260](https://github.com/matrix-org/matrix-spec-proposals/pull/4260) report user API. \ No newline at end of file +Add support for the [MSC4260 user report API](https://github.com/matrix-org/matrix-spec-proposals/pull/4260). \ No newline at end of file diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index b192dec9f51..0a963b121a4 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -560,6 +560,3 @@ def read_config( # MSC4076: Add `disable_badge_count`` to pusher configuration self.msc4076_enabled: bool = experimental.get("msc4076_enabled", False) - - # MSC4260: Report user API (Client-Server) - self.msc4260_enabled: bool = experimental.get("msc4260_enabled", False) diff --git a/synapse/rest/client/reporting.py b/synapse/rest/client/reporting.py index 9428fc093e5..17f4f120332 100644 --- a/synapse/rest/client/reporting.py +++ b/synapse/rest/client/reporting.py @@ -156,12 +156,11 @@ class ReportUserRestServlet(RestServlet): Introduced by MSC4260: https://github.com/matrix-org/matrix-spec-proposals/pull/4260 """ - # Cast the Iterable to a list so that we can `append` below. PATTERNS = list( client_patterns( - "/org.matrix.msc4260/users/(?P[^/]*)/report$", - releases=[], # unstable only - unstable=True, + "/users/(?P[^/]*)/report$", + releases=("v3",), + unstable=False, v1=False, ) ) @@ -206,6 +205,4 @@ async def on_POST( def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: ReportEventRestServlet(hs).register(http_server) ReportRoomRestServlet(hs).register(http_server) - - if hs.config.experimental.msc4260_enabled: - ReportUserRestServlet(hs).register(http_server) + ReportUserRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index caad93104e7..266a0b835b9 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -174,8 +174,6 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "org.matrix.simplified_msc3575": msc3575_enabled, # Arbitrary key-value profile fields. "uk.tcpip.msc4133": self.config.experimental.msc4133_enabled, - # MSC4260: Report users API (Client-Server) - "org.matrix.msc4260": self.config.experimental.msc4260_enabled, }, }, ) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 86ca1a3a166..7bc6150e67b 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -217,9 +217,8 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.other_user_tok = self.login("user", "pass") self.target_user_id = self.register_user("target_user", "pass") - self.report_path = f"/_matrix/client/unstable/org.matrix.msc4260/users/{self.target_user_id}/report" + self.report_path = f"/_matrix/client/v3/users/{self.target_user_id}/report" - @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_reason_str(self) -> None: data = {"reason": "this makes me sad"} self._assert_status(200, data) @@ -234,29 +233,25 @@ def test_reason_str(self) -> None: ) self.assertEqual(len(rows), 1) - @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_no_reason(self) -> None: data = {"not_reason": "for typechecking"} self._assert_status(400, data) - @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_reason_nonstring(self) -> None: data = {"reason": 42} self._assert_status(400, data) - @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_reason_null(self) -> None: data = {"reason": None} self._assert_status(400, data) - @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_cannot_report_nonlcoal_user(self) -> None: """ Tests that we don't accept event reports for users which aren't local users. """ channel = self.make_request( "POST", - "/_matrix/client/unstable/org.matrix.msc4260/users/@bloop:example.org/report", + "/_matrix/client/v3/users/@bloop:example.org/report", {"reason": "i am very sad"}, access_token=self.other_user_tok, shorthand=False, @@ -268,7 +263,6 @@ def test_cannot_report_nonlcoal_user(self) -> None: msg=channel.result["body"], ) - @override_config({"experimental_features": {"msc4260_enabled": True}}) def test_can_report_nonexistent_user(self) -> None: """ Tests that we ignore reports for nonexistent users. @@ -276,7 +270,7 @@ def test_can_report_nonexistent_user(self) -> None: target_user_id = f"@bloop:{self.hs.hostname}" channel = self.make_request( "POST", - f"/_matrix/client/unstable/org.matrix.msc4260/users/{target_user_id}/report", + f"/_matrix/client/v3/users/{target_user_id}/report", {"reason": "i am very sad"}, access_token=self.other_user_tok, shorthand=False, From 6ce8cc06c62004c606868b402ccf1ebe8f88c60c Mon Sep 17 00:00:00 2001 From: turt2live <1190097+turt2live@users.noreply.github.com> Date: Tue, 18 Mar 2025 21:12:37 +0000 Subject: [PATCH 15/28] Attempt to fix linting --- tests/rest/client/test_reporting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 7bc6150e67b..80281a2e758 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -28,7 +28,6 @@ from synapse.util import Clock from tests import unittest -from tests.unittest import override_config class ReportEventTestCase(unittest.HomeserverTestCase): From 42b4207134b044b4f3fee5a1ebd25600822312e7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Mar 2025 15:12:49 -0600 Subject: [PATCH 16/28] Empty commit to fix CI From 386a9e6faaff19a0ce42e24098a7ba965a89c600 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 May 2025 11:13:01 -0600 Subject: [PATCH 17/28] Apply suggestions from code review Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- synapse/storage/databases/main/room.py | 2 +- tests/rest/client/test_reporting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index d0c57b29d84..242995885c9 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -2560,7 +2560,7 @@ async def add_user_report( reason: Description that the user specifies. received_ts: Time when the user submitted the report (milliseconds). Returns: - Id of the room report. + ID of the room report. """ next_id = self._user_reports_id_gen.get_next() await self.db_pool.simple_insert( diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 80281a2e758..f4d390d497a 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -244,7 +244,7 @@ def test_reason_null(self) -> None: data = {"reason": None} self._assert_status(400, data) - def test_cannot_report_nonlcoal_user(self) -> None: + def test_cannot_report_nonlocal_user(self) -> None: """ Tests that we don't accept event reports for users which aren't local users. """ From c394ad1290b8198d5f1cc41259654bae9a16d28e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 May 2025 11:16:13 -0600 Subject: [PATCH 18/28] Fix local user check --- synapse/rest/client/reporting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/rest/client/reporting.py b/synapse/rest/client/reporting.py index 17f4f120332..2114fec89af 100644 --- a/synapse/rest/client/reporting.py +++ b/synapse/rest/client/reporting.py @@ -183,9 +183,9 @@ async def on_POST( body = parse_and_validate_json_object_from_request(request, self.PostBody) - # We can't deal with non-local users. + # Treat non-local users as though they don't exist. if not self.hs.is_mine_id(target_user_id): - raise NotFoundError("User does not belong to this server") + return 200, {} user = await self.store.get_user_by_id(target_user_id) if user is None: From 2f8958c1260b0f3dccb732aca7b18f26c6bd33c6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 May 2025 11:17:42 -0600 Subject: [PATCH 19/28] Limit length of `reason`; add rate limit; move to handler --- .../configuration/config_documentation.md | 19 +++++ synapse/config/ratelimiting.py | 6 ++ synapse/handlers/reports.py | 74 +++++++++++++++++++ synapse/rest/client/reporting.py | 19 +---- synapse/server.py | 4 + tests/rest/client/test_reporting.py | 44 +++++------ 6 files changed, 123 insertions(+), 43 deletions(-) create mode 100644 synapse/handlers/reports.py diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index 5351bef83aa..204a7e37972 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -1970,6 +1970,25 @@ rc_delayed_event_mgmt: burst_count: 20 ``` --- +### `rc_reports` + +Ratelimiting settings for reporting content. + +This is a ratelimiting option that ratelimits reports made by users +about content they see. + +It defaults to: `per_second: 1`, `burst_count: 5`. + +Setting this to a high value allows users to report content quickly, possibly in +duplicate. This can result in higher database usage. + +Example configuration: +```yaml +rc_reports: + per_second: 2 + burst_count: 20 +``` +--- ### `federation_rr_transactions_per_room_per_second` Sets outgoing federation transaction frequency for sending read-receipts, diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py index eb1dc2dacbf..290701615f5 100644 --- a/synapse/config/ratelimiting.py +++ b/synapse/config/ratelimiting.py @@ -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}, + ) diff --git a/synapse/handlers/reports.py b/synapse/handlers/reports.py new file mode 100644 index 00000000000..ea38156ba93 --- /dev/null +++ b/synapse/handlers/reports.py @@ -0,0 +1,74 @@ +# +# 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: +# . +# +# +import logging +from http import HTTPStatus +from typing import TYPE_CHECKING + +from synapse.api.errors import SynapseError, Codes +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: + 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 + + 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(), + ) diff --git a/synapse/rest/client/reporting.py b/synapse/rest/client/reporting.py index 2114fec89af..484827d9f2b 100644 --- a/synapse/rest/client/reporting.py +++ b/synapse/rest/client/reporting.py @@ -171,6 +171,7 @@ def __init__(self, hs: "HomeServer"): 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 @@ -179,25 +180,9 @@ async def on_POST( self, request: SynapseRequest, target_user_id: str ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) - user_id = requester.user.to_string() - body = parse_and_validate_json_object_from_request(request, self.PostBody) - # Treat non-local users as though they don't exist. - if not self.hs.is_mine_id(target_user_id): - return 200, {} - - user = await self.store.get_user_by_id(target_user_id) - if user is None: - # raise NotFoundError("User does not exist") - return 200, {} # hide existence - - await self.store.add_user_report( - target_user_id=target_user_id, - user_id=user_id, - reason=body.reason, - received_ts=self.clock.time_msec(), - ) + await self.handler.report_user(requester, target_user_id, body.reason) return 200, {} diff --git a/synapse/server.py b/synapse/server.py index bd2faa61b94..4d4eb2b5834 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -717,6 +717,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) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index f4d390d497a..be04ddd0cb7 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -216,7 +216,6 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.other_user_tok = self.login("user", "pass") self.target_user_id = self.register_user("target_user", "pass") - self.report_path = f"/_matrix/client/v3/users/{self.target_user_id}/report" def test_reason_str(self) -> None: data = {"reason": "this makes me sad"} @@ -244,52 +243,45 @@ def test_reason_null(self) -> None: data = {"reason": None} self._assert_status(400, data) + def test_reason_long(self) -> None: + data = {"reason": "x" * 1001} + self._assert_status(400, data) + def test_cannot_report_nonlocal_user(self) -> None: """ - Tests that we don't accept event reports for users which aren't local users. + Tests that we ignore reports for nonlocal users. """ - channel = self.make_request( - "POST", - "/_matrix/client/v3/users/@bloop:example.org/report", - {"reason": "i am very sad"}, - access_token=self.other_user_tok, - shorthand=False, - ) - self.assertEqual(404, channel.code, msg=channel.result["body"]) - self.assertEqual( - "User does not belong to this server", - channel.json_body["error"], - msg=channel.result["body"], - ) + target_user_id = "@bloop:example.org" + data = {"reason": "i am very sad"} + self._assert_status(200, data, target_user_id) + self._assert_no_reports_for_user(target_user_id) def test_can_report_nonexistent_user(self) -> None: """ Tests that we ignore reports for nonexistent users. """ target_user_id = f"@bloop:{self.hs.hostname}" - channel = self.make_request( - "POST", - f"/_matrix/client/v3/users/{target_user_id}/report", - {"reason": "i am very sad"}, - access_token=self.other_user_tok, - shorthand=False, - ) - self.assertEqual(200, channel.code, msg=channel.result["body"]) + data = {"reason": "i am very sad"} + self._assert_status(200, data, target_user_id) + self._assert_no_reports_for_user(target_user_id) + def _assert_no_reports_for_user(self, target_user_id: str) -> None: rows = self.get_success( self.hs.get_datastores().main.db_pool.simple_select_onecol( table="user_reports", - keyvalues={"target_user_id": self.target_user_id}, + keyvalues={"target_user_id": target_user_id}, retcol="id", desc="get_user_report_ids", ) ) self.assertEqual(len(rows), 0) - def _assert_status(self, response_status: int, data: JsonDict) -> None: + def _assert_status(self, response_status: int, data: JsonDict, user_id=None) -> None: + if user_id is None: + user_id = self.target_user_id channel = self.make_request( "POST", - self.report_path, + f"/_matrix/client/v3/users/{user_id}/report", data, access_token=self.other_user_tok, shorthand=False, From 9251b456717ca45655d472bac6f7c369c5d190b3 Mon Sep 17 00:00:00 2001 From: turt2live <1190097+turt2live@users.noreply.github.com> Date: Fri, 2 May 2025 17:46:50 +0000 Subject: [PATCH 20/28] Attempt to fix linting --- synapse/handlers/reports.py | 6 ++++-- tests/rest/client/test_reporting.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/reports.py b/synapse/handlers/reports.py index ea38156ba93..c2cc6c5c559 100644 --- a/synapse/handlers/reports.py +++ b/synapse/handlers/reports.py @@ -17,7 +17,7 @@ from http import HTTPStatus from typing import TYPE_CHECKING -from synapse.api.errors import SynapseError, Codes +from synapse.api.errors import Codes, SynapseError from synapse.api.ratelimiting import Ratelimiter from synapse.types import ( Requester, @@ -43,7 +43,9 @@ def __init__(self, hs: "HomeServer"): cfg=hs.config.ratelimiting.rc_reports, ) - async def report_user(self, requester: Requester, target_user_id: str, reason: str) -> None: + async def report_user( + self, requester: Requester, target_user_id: str, reason: str + ) -> None: await self._check_limits(requester) if len(reason) > 1000: diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index be04ddd0cb7..4a1f4f2b31d 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -276,7 +276,9 @@ def _assert_no_reports_for_user(self, target_user_id: str) -> None: ) self.assertEqual(len(rows), 0) - def _assert_status(self, response_status: int, data: JsonDict, user_id=None) -> None: + def _assert_status( + self, response_status: int, data: JsonDict, user_id=None + ) -> None: if user_id is None: user_id = self.target_user_id channel = self.make_request( From ff53217d87d56543058456c96ca5467b36f67e7e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 May 2025 11:47:04 -0600 Subject: [PATCH 21/28] Empty commit to kick CI From 51aceab878abf58e353904cc6a537f934a14acff Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 May 2025 11:49:18 -0600 Subject: [PATCH 22/28] move delta again --- .../storage/schema/main/delta/{90 => 91}/02_add_user_reports.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename synapse/storage/schema/main/delta/{90 => 91}/02_add_user_reports.sql (100%) diff --git a/synapse/storage/schema/main/delta/90/02_add_user_reports.sql b/synapse/storage/schema/main/delta/91/02_add_user_reports.sql similarity index 100% rename from synapse/storage/schema/main/delta/90/02_add_user_reports.sql rename to synapse/storage/schema/main/delta/91/02_add_user_reports.sql From ba32d12528cdfc2364a1662056399175d057ecf6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 May 2025 11:49:32 -0600 Subject: [PATCH 23/28] I guess imports are important --- synapse/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/server.py b/synapse/server.py index 4d4eb2b5834..2e6887640dd 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -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, From 223df1474885061c5071e8a2baedb98dc3a6d1a0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 May 2025 11:54:08 -0600 Subject: [PATCH 24/28] Add types --- tests/rest/client/test_reporting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rest/client/test_reporting.py b/tests/rest/client/test_reporting.py index 4a1f4f2b31d..9335d46eb5c 100644 --- a/tests/rest/client/test_reporting.py +++ b/tests/rest/client/test_reporting.py @@ -18,6 +18,7 @@ # [This file includes modifications made by New Vector Limited] # # +from typing import Optional from twisted.test.proto_helpers import MemoryReactor @@ -277,7 +278,7 @@ def _assert_no_reports_for_user(self, target_user_id: str) -> None: self.assertEqual(len(rows), 0) def _assert_status( - self, response_status: int, data: JsonDict, user_id=None + self, response_status: int, data: JsonDict, user_id: Optional[str] = None ) -> None: if user_id is None: user_id = self.target_user_id From cf0353551efff73259644941bf64440b6a94acad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 19 Jun 2025 10:32:24 -0600 Subject: [PATCH 25/28] add docs --- synapse/handlers/reports.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/synapse/handlers/reports.py b/synapse/handlers/reports.py index c2cc6c5c559..a7b8a4bed74 100644 --- a/synapse/handlers/reports.py +++ b/synapse/handlers/reports.py @@ -46,6 +46,28 @@ def __init__(self, hs: "HomeServer"): 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: From 746c506ad8988495f80dae87eb7dd4008bb7052d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 19 Jun 2025 10:34:38 -0600 Subject: [PATCH 26/28] Add second index; move to new delta --- .../{91/02_add_user_reports.sql => 92/07_add_user_reports.sql} | 1 + 1 file changed, 1 insertion(+) rename synapse/storage/schema/main/delta/{91/02_add_user_reports.sql => 92/07_add_user_reports.sql} (91%) diff --git a/synapse/storage/schema/main/delta/91/02_add_user_reports.sql b/synapse/storage/schema/main/delta/92/07_add_user_reports.sql similarity index 91% rename from synapse/storage/schema/main/delta/91/02_add_user_reports.sql rename to synapse/storage/schema/main/delta/92/07_add_user_reports.sql index 2521aefc51c..7439dad6d6b 100644 --- a/synapse/storage/schema/main/delta/91/02_add_user_reports.sql +++ b/synapse/storage/schema/main/delta/92/07_add_user_reports.sql @@ -19,3 +19,4 @@ CREATE TABLE user_reports ( 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 From 7d46b07fb29cdcd2893db1daf7f54e1d447ca8a7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 19 Jun 2025 11:26:59 -0600 Subject: [PATCH 27/28] move config --- .../configuration/config_documentation.md | 19 ------------------- schema/synapse-config.schema.yaml | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index bbf219c6ceb..860837cae56 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -1937,25 +1937,6 @@ rc_delayed_event_mgmt: burst_count: 20.0 ``` --- -### `rc_reports` - -Ratelimiting settings for reporting content. - -This is a ratelimiting option that ratelimits reports made by users -about content they see. - -It defaults to: `per_second: 1`, `burst_count: 5`. - -Setting this to a high value allows users to report content quickly, possibly in -duplicate. This can result in higher database usage. - -Example configuration: -```yaml -rc_reports: - per_second: 2 - burst_count: 20 -``` ---- ### `federation_rr_transactions_per_room_per_second` *(integer)* Sets outgoing federation transaction frequency for sending read-receipts, per-room. diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index 52a6d5cf71b..8a5c6850494 100644 --- a/schema/synapse-config.schema.yaml +++ b/schema/synapse-config.schema.yaml @@ -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: >- From c4cdded23c31040dacaedbad37b18f45a7e7bc0d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 19 Jun 2025 11:54:16 -0600 Subject: [PATCH 28/28] Use generated config --- .../configuration/config_documentation.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index 860837cae56..257ea4a1a25 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -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.