Skip to content

Commit

Permalink
Add report room API (MSC4151) (element-hq#17270)
Browse files Browse the repository at this point in the history
matrix-org/matrix-spec-proposals#4151

This is intended to be enabled by default for immediate use. When FCP is
complete, the unstable endpoint will be dropped and stable endpoint
supported instead - no backwards compatibility is expected for the
unstable endpoint.
  • Loading branch information
turt2live authored and Mic92 committed Jun 13, 2024
1 parent 8e61957 commit adfce3a
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 8 deletions.
1 change: 1 addition & 0 deletions changelog.d/17270.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for the unstable [MSC4151](https://github.com/matrix-org/matrix-spec-proposals/pull/4151) report room API.
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
self.msc3916_authenticated_media_enabled = experimental.get(
"msc3916_authenticated_media_enabled", False
)

# MSC4151: Report room API (Client-Server API)
self.msc4151_enabled: bool = experimental.get("msc4151_enabled", False)
4 changes: 2 additions & 2 deletions synapse/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
register,
relations,
rendezvous,
report_event,
reporting,
room,
room_keys,
room_upgrade_rest_servlet,
Expand Down Expand Up @@ -128,7 +128,7 @@ def register_servlets(client_resource: HttpServer, hs: "HomeServer") -> None:
tags.register_servlets(hs, client_resource)
account_data.register_servlets(hs, client_resource)
if is_main_process:
report_event.register_servlets(hs, client_resource)
reporting.register_servlets(hs, client_resource)
openid.register_servlets(hs, client_resource)
notifications.register_servlets(hs, client_resource)
devices.register_servlets(hs, client_resource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,28 @@
from http import HTTPStatus
from typing import TYPE_CHECKING, Tuple

from synapse._pydantic_compat import HAS_PYDANTIC_V2
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.http.servlet import (
RestServlet,
parse_and_validate_json_object_from_request,
parse_json_object_from_request,
)
from synapse.http.site import SynapseRequest
from synapse.types import JsonDict
from synapse.types.rest import RequestBodyModel

from ._base import client_patterns

if TYPE_CHECKING:
from synapse.server import HomeServer

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import StrictStr
else:
from pydantic import StrictStr

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -95,5 +106,49 @@ async def on_POST(
return 200, {}


class ReportRoomRestServlet(RestServlet):
# https://github.com/matrix-org/matrix-spec-proposals/pull/4151
PATTERNS = client_patterns(
"/org.matrix.msc4151/rooms/(?P<room_id>[^/]*)/report$",
releases=[],
v1=False,
unstable=True,
)

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, room_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)

room = await self.store.get_room(room_id)
if room is None:
raise NotFoundError("Room does not exist")

await self.store.add_room_report(
room_id=room_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)

if hs.config.experimental.msc4151_enabled:
ReportRoomRestServlet(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 @@ -149,6 +149,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
is not None
)
),
# MSC4151: Report room API (Client-Server API)
"org.matrix.msc4151": self.config.experimental.msc4151_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 @@ -2207,6 +2207,7 @@ def __init__(
super().__init__(database, db_conn, hs)

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

self._instance_name = hs.get_instance_name()

Expand Down Expand Up @@ -2416,6 +2417,37 @@ async def add_event_report(
)
return next_id

async def add_room_report(
self,
room_id: str,
user_id: str,
reason: str,
received_ts: int,
) -> int:
"""Add a room report
Args:
room_id: The room ID being reported.
user_id: User who reports the room.
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._room_reports_id_gen.get_next()
await self.db_pool.simple_insert(
table="room_reports",
values={
"id": next_id,
"received_ts": received_ts,
"room_id": room_id,
"user_id": user_id,
"reason": reason,
},
desc="add_room_report",
)
return next_id

async def block_room(self, room_id: str, user_id: str) -> None:
"""Marks the room as blocked.
Expand Down
20 changes: 20 additions & 0 deletions synapse/storage/schema/main/delta/85/06_add_room_reports.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2024 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 room_reports (
id BIGINT NOT NULL PRIMARY KEY,
received_ts BIGINT NOT NULL,
room_id TEXT NOT NULL,
user_id TEXT NOT NULL,
reason TEXT NOT NULL
);
6 changes: 3 additions & 3 deletions tests/rest/admin/test_event_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

import synapse.rest.admin
from synapse.api.errors import Codes
from synapse.rest.client import login, report_event, room
from synapse.rest.client import login, reporting, room
from synapse.server import HomeServer
from synapse.types import JsonDict
from synapse.util import Clock
Expand All @@ -37,7 +37,7 @@ class EventReportsTestCase(unittest.HomeserverTestCase):
synapse.rest.admin.register_servlets,
login.register_servlets,
room.register_servlets,
report_event.register_servlets,
reporting.register_servlets,
]

def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
Expand Down Expand Up @@ -453,7 +453,7 @@ class EventReportDetailTestCase(unittest.HomeserverTestCase):
synapse.rest.admin.register_servlets,
login.register_servlets,
room.register_servlets,
report_event.register_servlets,
reporting.register_servlets,
]

def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from twisted.test.proto_helpers import MemoryReactor

import synapse.rest.admin
from synapse.rest.client import login, report_event, room
from synapse.rest.client import login, reporting, room
from synapse.server import HomeServer
from synapse.types import JsonDict
from synapse.util import Clock
Expand All @@ -35,7 +35,7 @@ class ReportEventTestCase(unittest.HomeserverTestCase):
synapse.rest.admin.register_servlets,
login.register_servlets,
room.register_servlets,
report_event.register_servlets,
reporting.register_servlets,
]

def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
Expand Down Expand Up @@ -139,3 +139,92 @@ def _assert_status(self, response_status: int, data: JsonDict) -> None:
"POST", self.report_path, data, access_token=self.other_user_tok
)
self.assertEqual(response_status, channel.code, msg=channel.result["body"])


class ReportRoomTestCase(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.room_id = self.helper.create_room_as(
self.other_user, tok=self.other_user_tok, is_public=True
)
self.report_path = (
f"/_matrix/client/unstable/org.matrix.msc4151/rooms/{self.room_id}/report"
)

@unittest.override_config(
{
"experimental_features": {"msc4151_enabled": True},
}
)
def test_reason_str(self) -> None:
data = {"reason": "this makes me sad"}
self._assert_status(200, data)

@unittest.override_config(
{
"experimental_features": {"msc4151_enabled": True},
}
)
def test_no_reason(self) -> None:
data = {"not_reason": "for typechecking"}
self._assert_status(400, data)

@unittest.override_config(
{
"experimental_features": {"msc4151_enabled": True},
}
)
def test_reason_nonstring(self) -> None:
data = {"reason": 42}
self._assert_status(400, data)

@unittest.override_config(
{
"experimental_features": {"msc4151_enabled": True},
}
)
def test_reason_null(self) -> None:
data = {"reason": None}
self._assert_status(400, data)

@unittest.override_config(
{
"experimental_features": {"msc4151_enabled": True},
}
)
def test_cannot_report_nonexistent_room(self) -> None:
"""
Tests that we don't accept event reports for rooms which do not exist.
"""
channel = self.make_request(
"POST",
"/_matrix/client/unstable/org.matrix.msc4151/rooms/!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(
"Room does not exist",
channel.json_body["error"],
msg=channel.result["body"],
)

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"])

0 comments on commit adfce3a

Please sign in to comment.