Skip to content
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

Add report user API from MSC4260 #18120

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
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 unstable [MSC4260](https://github.com/matrix-org/matrix-spec-proposals/pull/4260) report user API.
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,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)
56 changes: 56 additions & 0 deletions synapse/rest/client/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<target_user_id>[^/]*)/report$",
releases=[], # unstable only
unstable=True,
v1=False,
)
)
Comment on lines +159 to +167
Copy link
Member

Choose a reason for hiding this comment

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

This looks like to be a copy-paste from the room report API, when it got stabilised: https://github.com/element-hq/synapse/pull/17374/files#diff-c9b74d46cac48932cc854ad791337faad187cbbdb6bd64682b1bcc7740bfc563

Copy link
Member Author

Choose a reason for hiding this comment

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

it is :D

is that an issue?


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)
2 changes: 2 additions & 0 deletions synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
)
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 @@ -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()

Expand Down Expand Up @@ -2544,6 +2545,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
21 changes: 21 additions & 0 deletions synapse/storage/schema/main/delta/88/07_add_user_reports.sql
Copy link
Member

Choose a reason for hiding this comment

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

Could you merge back develop & move this to the latest schema directory?

Original file line number Diff line number Diff line change
@@ -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:
-- <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
101 changes: 101 additions & 0 deletions tests/rest/client/test_reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from synapse.util import Clock

from tests import unittest
from tests.unittest import override_config


class ReportEventTestCase(unittest.HomeserverTestCase):
Expand Down Expand Up @@ -201,3 +202,103 @@ def _assert_status(self, response_status: int, data: JsonDict) -> None:
shorthand=False,
)
self.assertEqual(response_status, channel.code, msg=channel.result["body"])


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"

@override_config({"experimental_features": {"msc4260_enabled": True}})
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",
)
)
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",
{"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"],
)

@override_config({"experimental_features": {"msc4260_enabled": True}})
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"])

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(
"POST",
self.report_path,
data,
access_token=self.other_user_tok,
shorthand=False,
)
self.assertEqual(response_status, channel.code, msg=channel.result["body"])
Loading