mirror of
https://github.com/element-hq/synapse.git
synced 2025-03-14 09:45:51 +00:00
Merge dbe43a1283
into caa2012154
This commit is contained in:
commit
ca5c170c43
7 changed files with 216 additions and 0 deletions
1
changelog.d/18120.feature
Normal file
1
changelog.d/18120.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add support for the unstable [MSC4260](https://github.com/matrix-org/matrix-spec-proposals/pull/4260) report user API.
|
|
@ -560,3 +560,6 @@ class ExperimentalConfig(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)
|
||||
|
|
|
@ -150,6 +150,62 @@ class ReportRoomRestServlet(RestServlet):
|
|||
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,
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
|
@ -174,6 +174,8 @@ class VersionsRestServlet(RestServlet):
|
|||
"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,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
@ -2303,6 +2303,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore):
|
|||
|
||||
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,37 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore):
|
|||
)
|
||||
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.
|
||||
|
||||
|
|
21
synapse/storage/schema/main/delta/88/07_add_user_reports.sql
Normal file
21
synapse/storage/schema/main/delta/88/07_add_user_reports.sql
Normal file
|
@ -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
|
|
@ -28,6 +28,7 @@ from synapse.types import JsonDict
|
|||
from synapse.util import Clock
|
||||
|
||||
from tests import unittest
|
||||
from tests.unittest import override_config
|
||||
|
||||
|
||||
class ReportEventTestCase(unittest.HomeserverTestCase):
|
||||
|
@ -201,3 +202,103 @@ class ReportRoomTestCase(unittest.HomeserverTestCase):
|
|||
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…
Add table
Reference in a new issue