Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Admin API for reported events #8217

Merged
merged 9 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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/8217.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an admin API `GET /_synapse/admin/v1/event_reports` to read entries of table `event_reports`. Contributed by @dklimpel.
122 changes: 122 additions & 0 deletions docs/admin_api/event_reports.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
Show reported events
====================

This API returns information about reported events.

The api is::

GET /_synapse/admin/v1/event_reports?from=0&limit=10

To use it, you will need to authenticate by providing an ``access_token`` for a
server admin: see `README.rst <README.rst>`_.

It returns a JSON body like the following:

.. code:: json

{
"event_reports": [
{
"content": {
"reason": "foo",
"score": -100
},
"event_id": "$bNUFCwGzWca1meCGkjp-zwslF-GfVcXukvRLI1_FaVY",
"event_json": {
"auth_events": [
"$YK4arsKKcc0LRoe700pS8DSjOvUT4NDv0HfInlMFw2M",
"$oggsNXxzPFRE3y53SUNd7nsj69-QzKv03a1RucHu-ws"
],
"content": {
"body": "matrix.org: This Week in Matrix",
"format": "org.matrix.custom.html",
"formatted_body": "<strong>matrix.org</strong>:<br><a href=\"https://matrix.org/blog/\"><strong>This Week in Matrix</strong></a>",
"msgtype": "m.notice"
},
"depth": 546,
"hashes": {
"sha256": "xK1//xnmvHJIOvbgXlkI8eEqdvoMmihVDJ9J4SNlsAw"
},
"origin": "matrix.org",
"origin_server_ts": 1592291711430,
"prev_events": [
"$YK4arsKKcc0LRoe700pS8DSjOvUT4NDv0HfInlMFw2M"
],
"prev_state": [],
"room_id": "!ERAgBpSOcCCuTJqQPk:matrix.org",
"sender": "@foobar:matrix.org",
"signatures": {
"matrix.org": {
"ed25519:a_JaEG": "cs+OUKW/iHx5pEidbWxh0UiNNHwe46Ai9LwNz+Ah16aWDNszVIe2gaAcVZfvNsBhakQTew51tlKmL2kspXk/Dg"
}
},
"type": "m.room.message",
"unsigned": {
"age_ts": 1592291711430,
}
},
"id": 2,
"reason": "foo",
"received_ts": 1570897107409,
"room_alias": "#alias1:matrix.org",
"room_id": "!ERAgBpSOcCCuTJqQPk:matrix.org",
"sender": "@foobar:matrix.org",
"user_id": "@foo:matrix.org"
},
{
"content": {
"reason": "bar",
"score": -100
},
"event_id": "$3IcdZsDaN_En-S1DF4EMCy3v4gNRKeOJs8W5qTOKj4I",
"event_json": {
"_comment": "... (hidden items) ..."
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
},
"id": 3,
"reason": "bar",
"received_ts": 1598889612059,
"room_alias": "#alias2:matrix.org",
"room_id": "!eGvUQuTCkHGVwNMOjv:matrix.org",
"sender": "@foobar:matrix.org",
"user_id": "@bar:matrix.org"
}
],
"next_token": "2",
"total": 4
}

To paginate, check for ``next_token`` and if present, call the endpoint again
with ``from`` set to the value of ``next_token``. This will return a new page.

If the endpoint does not return a ``next_token`` then there are no more
reports to paginate through.

**URL parameters:**
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved

- ``limit``: Is optional but is used for pagination,
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
denoting the maximum number of items to return in this call. Defaults to ``100``.
- ``from``: Is optional but used for pagination,
denoting the offset in the returned results. This should be treated as an opaque value and
not explicitly set to anything other than the return value of ``next_token`` from a previous call.
Defaults to ``0``.
- ``dir`` - Direction of event report order. Whether to fetch the most recent first (``b``) or the
oldest first (``f``). Defaults to ``b``.
- ``user_id``: Is optional and filters to only return users with user IDs that contain this value.
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
This is the user who reported the event and wrote the reason.
- ``room_id``: Is optional and filters to only return rooms with room IDs that contain this value.

**Response**

The following fields are returned in the JSON response body:

- ``id``: Id of event report.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
- ``received_ts``: The timestamp (in milliseconds since the unix epoch) when this report was sent.
- ``room_id``: The ID of the room.
- ``event_id``: The ID of the reported event.
- ``user_id``: This is the user who reported the event and wrote the reason.
- ``reason``: Comment made by the ``user_id`` in this report.
- ``content``: Content of reported event.
- ``sender``: This is the ID of the user who sent the original message/event that was reported.
- ``room_alias``: The alias of the room.
Copy link

Choose a reason for hiding this comment

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

Should it be mentioned here, that this field can be null, if no room alias is specified (e.g. in direct messages)?

Copy link
Member

Choose a reason for hiding this comment

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

Sounds helpful. I would write something like "The alias of the room. null if the room does not have a canonical alias set."

- ``event_json``: Details of the original event that was reported.

2 changes: 2 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
DeviceRestServlet,
DevicesRestServlet,
)
from synapse.rest.admin.event_reports import EventReportsRestServlet
from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet
Expand Down Expand Up @@ -214,6 +215,7 @@ def register_servlets(hs, http_server):
DeviceRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
DeleteDevicesRestServlet(hs).register(http_server)
EventReportsRestServlet(hs).register(http_server)


def register_servlets_for_client_rest_resource(hs, http_server):
Expand Down
88 changes: 88 additions & 0 deletions synapse/rest/admin/event_reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Copyright 2020 Dirk Klimpel
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from synapse.api.errors import Codes, SynapseError
from synapse.http.servlet import RestServlet, parse_integer, parse_string
from synapse.rest.admin._base import admin_patterns, assert_requester_is_admin

logger = logging.getLogger(__name__)


class EventReportsRestServlet(RestServlet):
"""
List all reported events that are known to the homeserver. Results are returned
in a dictionary containing report information. Supports pagination.
This needs user to have administrator access in Synapse.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

GET /_synapse/admin/v1/event_reports
returns:
200 OK with list of reports if success otherwise an error.

Args:
The parameters `from` and `limit` are required only for pagination.
By default, a `limit` of 100 is used.
The parameter `dir` can be used to define the order of results.
The parameter `user_id` can be used to filter by user id.
The parameter `room_id` can be used to filter by room id.
Returns:
A list of reported events and an integer representing the total number of
reported events that exist given this query
"""

PATTERNS = admin_patterns("/event_reports$")

def __init__(self, hs):
self.hs = hs
self.auth = hs.get_auth()
self.store = hs.get_datastore()

async def on_GET(self, request):
await assert_requester_is_admin(self.auth, request)

start = parse_integer(request, "from", default=0)
Copy link
Member

Choose a reason for hiding this comment

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

We may want to do some basic error checking for negative from values. SQLite3 won't mind (will treat it as 0, but Postgres will raise an error).

limit = parse_integer(request, "limit", default=100)
direction = parse_string(request, "dir", default="b")
user_id = parse_string(request, "user_id")
room_id = parse_string(request, "room_id")

if start < 0:
raise SynapseError(
400,
"The start parameter must be a positive integer.",
errcode=Codes.INVALID_PARAM,
)

if limit < 0:
raise SynapseError(
400,
"The limit parameter must be a positive integer.",
errcode=Codes.INVALID_PARAM,
)

if direction not in ("f", "b"):
raise SynapseError(
400, "Unknown direction: %s" % (direction,), errcode=Codes.INVALID_PARAM
)

event_reports, total = await self.store.get_event_reports_paginate(
start, limit, direction, user_id, room_id
)
ret = {"event_reports": event_reports, "total": total}
if len(event_reports) >= limit:
Copy link
Member

Choose a reason for hiding this comment

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

len(event_reports) should hopefully never be greater than limit. Perhaps you meant if start + len(event_reports) < total:?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can find the same code here:

ret = {"users": users, "total": total}
if len(users) >= limit:
ret["next_token"] = str(start + len(users))

If len(event_reports) equals limit you will get more results on next page.

Copy link
Member

Choose a reason for hiding this comment

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

I see, this was a bit confusing. == is really the case we care about here, but we're doing >= for safety.

So... does this mean if there are 100 reports, and we set a limit of 100, we'll get a next_token that doesn't actually show any more reports?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh. There is a bug. You are right. I will add a test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure. Should be next_token an integer or string?
In the user api it is a string:

ret["next_token"] = str(start + len(users))

In room api an integer:

response["next_batch"] = start + limit

Copy link
Member

Choose a reason for hiding this comment

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

Generally we prefer pagination tokens to be strings, so that we can encode extra data into them if necessary.

However, considering we're not very likely to do that here, and it would be unfortunate to have one admin API with a string pagination token, and another with an int one: I'd keep it as int for now.

In the future if we need to encode extra data for some reason, we'll just bump the endpoint version.

ret["next_token"] = str(start + len(event_reports))

return 200, ret
95 changes: 95 additions & 0 deletions synapse/storage/databases/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,101 @@ async def add_event_report(
desc="add_event_report",
)

async def get_event_reports_paginate(
self,
start: int,
limit: int,
direction: str = "b",
user_id: str = None,
room_id: str = None,
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
) -> Tuple[List[Dict[str, Any]], int]:
"""Retrieve a paginated list of event reports

Args:
start: event offset to begin the query from
limit: number of rows to retrieve
direction: Whether to fetch the most recent first (`"b"`) or the
oldest first (`"f"`).
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
user_id: search for user_id. Ignored if user_id is None
room_id: search for room_id. Ignored if room_id is None
Returns:
event_reports: json list of event reports
count: total number of event reports matching the filter criteria
"""

def _get_event_reports_paginate_txn(txn):
filters = []
args = []

if user_id:
filters.append("er.user_id LIKE ?")
args.extend(["%" + user_id + "%"])
if room_id:
filters.append("er.room_id LIKE ?")
args.extend(["%" + room_id + "%"])

if direction == "b":
order = "DESC"
else:
order = "ASC"

where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else ""

sql = """
SELECT COUNT(*) as total_event_reports
FROM event_reports AS er
{}
""".format(
where_clause
)
txn.execute(sql, args)
count = txn.fetchone()[0]

sql = """
SELECT
er.id,
er.received_ts,
er.room_id,
er.event_id,
er.user_id,
er.reason,
er.content,
events.sender,
room_aliases.room_alias,
event_json.json AS event_json
FROM event_reports AS er
LEFT JOIN room_aliases
ON room_aliases.room_id = er.room_id
JOIN events
ON events.event_id = er.event_id
JOIN event_json
ON event_json.event_id = er.event_id
{where_clause}
ORDER BY er.received_ts {order}
LIMIT ?
OFFSET ?
""".format(
where_clause=where_clause, order=order,
)

args += [limit, start]
txn.execute(sql, args)
event_reports = self.db_pool.cursor_to_dict(txn)

if count > 0:
for row in event_reports:
try:
row["content"] = db_to_json(row["content"])
row["event_json"] = db_to_json(row["event_json"])
except Exception:
continue

return event_reports, count

return await self.db_pool.runInteraction(
"get_event_reports_paginate", _get_event_reports_paginate_txn
)

def get_current_public_room_stream_id(self):
return self._public_room_id_gen.get_current_token()

Expand Down
Loading