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 2 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.
57 changes: 57 additions & 0 deletions docs/admin_api/event_reports.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Shows reported events
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
====================

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",
"id": 2,
"reason": "foo",
"received_ts": 1570897107409,
"room_id": "!ERAgBpSOcCCuTJqQPk:matrix.org",
"user_id": "@foo:matrix.org"
},
{
"content": "{\"score\":-100,\"reason\":\"bar\"}",
"event_id": "$3IcdZsDaN_En-S1DF4EMCy3v4gNRKeOJs8W5qTOKj4I",
"id": 3,
"reason": "bar",
"received_ts": 1598889612059,
"room_id": "!eGvUQuTCkHGVwNMOjv:matrix.org",
"user_id": "@bar:matrix.org"
}
],
"next_token": "2",
"total": 4
}
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

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.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

If the endpoint does not return a ``next_token`` then there are no more
users to paginate through.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

**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.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
Defaults to ``0``.
- ``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
- ``room_id``: Is optional and filters to only return rooms with room IDs that contain this value.
3 changes: 2 additions & 1 deletion synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# 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
import platform
import re
Expand All @@ -31,6 +30,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 +214,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
65 changes: 65 additions & 0 deletions synapse/rest/admin/event_reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# -*- 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.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?from=0&limit=10
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
returns:
200 OK with list of reports if success otherwise an error.

Args
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
The parameters `from` and `limit` are required only for pagination.
By default, a `limit` of 100 is used.
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)
user_id = parse_string(request, "user_id", default=None)
room_id = parse_string(request, "room_id", default=None)
Copy link
Member

Choose a reason for hiding this comment

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

The default for default is None.


event_reports, total = await self.store.get_event_reports_paginate(
start, limit, 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
52 changes: 52 additions & 0 deletions synapse/storage/databases/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,58 @@ async def add_event_report(
desc="add_event_report",
)

async def get_event_reports_paginate(
self, start: int, limit: int, user_id: str = None, room_id: str = None
) -> Tuple[List[Dict[str, Any]], int]:
"""Function to retrieve a paginated list of event reports
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
This will return a json list of event reports and the
total number of event reports matching the filter criteria.
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 put this in a Returns: block instead below Args:?


Args:
start: start number to begin the query from
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
limit: number of rows to retrieve
user_id: search for user_id. ignored if name is not None
room_id: search for room_id. ignored if name is not None
Copy link
Member

Choose a reason for hiding this comment

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

name?

"""

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

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

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

sql_base = """
FROM event_reports
{}
""".format(
where_clause
)
logger.warning("sql_base: %s ", sql_base)
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
sql = "SELECT COUNT(*) as total_event_reports " + sql_base
txn.execute(sql, args)
count = txn.fetchone()[0]

sql = (
"SELECT id, received_ts, room_id, event_id, user_id, reason, content "
+ sql_base
+ " ORDER BY received_ts LIMIT ? OFFSET ?"
)
args += [limit, start]
txn.execute(sql, args)
event_reports = self.db_pool.cursor_to_dict(txn)
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