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

Add admin API to list users' local media #8647

Merged
merged 9 commits into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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/8647.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an admin API `GET /_synapse/admin/v1/users/<user_id>/media` to get information about uploaded media. Contributed by @dklimpel.
81 changes: 81 additions & 0 deletions docs/admin_api/user_admin_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,87 @@ The following fields are returned in the JSON response body:
- ``total`` - Number of rooms.


List media of an user
================================
Gets a list of all local media that a specific ``user_id`` has created.

The API is::

GET /_synapse/admin/v1/users/<user_id>/media

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

A response body like the following is returned:

.. code:: json

{
"media": [
{
"created_ts": 100400,
"last_access_ts": null,
"media_id": "qXhyRzulkwLsNHTbpHreuEgo",
"media_length": 67,
"media_type": "image/png",
"quarantined_by": null,
"safe_from_quarantine": false,
"upload_name": "test1.png"
},
{
"created_ts": 200400,
"last_access_ts": null,
"media_id": "FHfiSnzoINDatrXHQIXBtahw",
"media_length": 67,
"media_type": "image/png",
"quarantined_by": null,
"safe_from_quarantine": false,
"upload_name": "test2.png"
}
],
"next_token": 3,
"total": 2
}

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.

**Parameters**

The following parameters should be set in the URL:

- ``user_id`` - string - fully qualified: for example, ``@user:server.com``.
- ``limit``: string representing a positive integer - Is optional but is used for pagination,
denoting the maximum number of items to return in this call. Defaults to ``100``.
- ``from``: string representing a positive integer - 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``.

**Response**

The following fields are returned in the JSON response body:

- ``media`` - An array of objects, each containing information about a media.
Media objects contain the following fields:

- ``created_ts`` - integer - Timestamp when the content was uploaded in ms.
- ``last_access_ts`` - integer - Timestamp when the content was last accessed in ms.
- ``media_id`` - string - The id used to refer to the media.
- ``media_length`` - integer - Length of the media in bytes.
- ``media_type`` - string - The MIME-type of the media.
- ``quarantined_by`` - string - The user ID that initiated the quarantine request
for this media.

- ``safe_from_quarantine`` - bool - Status if this media is safe from quarantining.
- ``upload_name`` - string - The name the media was uploaded with.

- ``next_token``: integer - Indication for pagination. See above.
- ``total`` - integer - Total number of media.

User devices
============

Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
ResetPasswordRestServlet,
SearchUsersRestServlet,
UserAdminServlet,
UserMediaRestServlet,
UserMembershipRestServlet,
UserRegisterServlet,
UserRestServletV2,
Expand Down Expand Up @@ -215,6 +216,7 @@ def register_servlets(hs, http_server):
SendServerNoticeServlet(hs).register(http_server)
VersionServlet(hs).register(http_server)
UserAdminServlet(hs).register(http_server)
UserMediaRestServlet(hs).register(http_server)
UserMembershipRestServlet(hs).register(http_server)
UserRestServletV2(hs).register(http_server)
UsersRestServletV2(hs).register(http_server)
Expand Down
67 changes: 66 additions & 1 deletion synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import hmac
import logging
from http import HTTPStatus
from typing import Tuple

from synapse.api.constants import UserTypes
from synapse.api.errors import Codes, NotFoundError, SynapseError
Expand All @@ -27,13 +28,14 @@
parse_json_object_from_request,
parse_string,
)
from synapse.http.site import SynapseRequest
from synapse.rest.admin._base import (
admin_patterns,
assert_requester_is_admin,
assert_user_is_admin,
historical_admin_path_patterns,
)
from synapse.types import UserID
from synapse.types import JsonDict, UserID

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -708,3 +710,66 @@ async def on_GET(self, request, user_id):

ret = {"joined_rooms": list(room_ids), "total": len(room_ids)}
return 200, ret


class UserMediaRestServlet(RestServlet):
"""
Gets information about all uploaded local media for a specific `user_id`.

Example:
http://localhost:8008/_synapse/admin/v1/users/
@user:server/media

Args:
The parameters `from` and `limit` are required for pagination.
By default, a `limit` of 100 is used.
Returns:
A list of media and an integer representing the total number of
media that exist given for this user
"""

PATTERNS = admin_patterns("/users/(?P<user_id>[^/]+)/media$")

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

async def on_GET(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self.auth, request)

if not self.is_mine(UserID.from_string(user_id)):
raise SynapseError(400, "Can only lookup local users")

user = await self.store.get_user_by_id(user_id)
if user is None:
raise NotFoundError("Unknown user")

start = parse_integer(request, "from", default=0)
limit = parse_integer(request, "limit", default=100)

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

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

media, total = await self.store.get_local_media_by_user_paginate(
start, limit, user_id
)

ret = {"media": media, "total": total}
if (start + limit) < total:
ret["next_token"] = start + len(media)

return 200, ret
50 changes: 50 additions & 0 deletions synapse/storage/databases/main/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,56 @@ async def get_local_media(self, media_id: str) -> Optional[Dict[str, Any]]:
desc="get_local_media",
)

async def get_local_media_by_user_paginate(
self, start: int, limit: int, user_id: str
) -> Tuple[List[Dict[str, Any]], int]:
"""Get a paginated list of metadata for a local piece of media
which an user_id has uploaded

Args:
start: offset in the list
limit: maximum amount of media_ids to retrieve
user_id: fully-qualified user id
Returns:
A paginated list of all metadata of user's media
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
"""

def get_local_media_by_user_paginate_txn(txn):
sql_base = """
FROM local_media_repository
WHERE user_id = ?
"""
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

args = [user_id]
sql = "SELECT COUNT(*) as total_media " + sql_base
txn.execute(sql, args)
count = txn.fetchone()[0]

sql = """
SELECT
"media_id",
"media_type",
"media_length",
"upload_name",
"created_ts",
"last_access_ts",
"quarantined_by",
"safe_from_quarantine"
{}
LIMIT ? OFFSET ?
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
""".format(
sql_base
)

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

return await self.db_pool.runInteraction(
"get_local_media_by_user_paginate_txn", get_local_media_by_user_paginate_txn
)

async def store_local_media(
self,
media_id,
Expand Down
Loading