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

Commit 9b7c282

Browse files
authored
Add admin API to list users' local media (#8647)
Add admin API `GET /_synapse/admin/v1/users/<user_id>/media` to get information of users' uploaded files.
1 parent 24229fa commit 9b7c282

File tree

8 files changed

+494
-1
lines changed

8 files changed

+494
-1
lines changed

changelog.d/8647.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add an admin API `GET /_synapse/admin/v1/users/<user_id>/media` to get information about uploaded media. Contributed by @dklimpel.

docs/admin_api/user_admin_api.rst

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,89 @@ The following fields are returned in the JSON response body:
341341
- ``total`` - Number of rooms.
342342

343343

344+
List media of an user
345+
================================
346+
Gets a list of all local media that a specific ``user_id`` has created.
347+
The response is ordered by creation date descending and media ID descending.
348+
The newest media is on top.
349+
350+
The API is::
351+
352+
GET /_synapse/admin/v1/users/<user_id>/media
353+
354+
To use it, you will need to authenticate by providing an ``access_token`` for a
355+
server admin: see `README.rst <README.rst>`_.
356+
357+
A response body like the following is returned:
358+
359+
.. code:: json
360+
361+
{
362+
"media": [
363+
{
364+
"created_ts": 100400,
365+
"last_access_ts": null,
366+
"media_id": "qXhyRzulkwLsNHTbpHreuEgo",
367+
"media_length": 67,
368+
"media_type": "image/png",
369+
"quarantined_by": null,
370+
"safe_from_quarantine": false,
371+
"upload_name": "test1.png"
372+
},
373+
{
374+
"created_ts": 200400,
375+
"last_access_ts": null,
376+
"media_id": "FHfiSnzoINDatrXHQIXBtahw",
377+
"media_length": 67,
378+
"media_type": "image/png",
379+
"quarantined_by": null,
380+
"safe_from_quarantine": false,
381+
"upload_name": "test2.png"
382+
}
383+
],
384+
"next_token": 3,
385+
"total": 2
386+
}
387+
388+
To paginate, check for ``next_token`` and if present, call the endpoint again
389+
with ``from`` set to the value of ``next_token``. This will return a new page.
390+
391+
If the endpoint does not return a ``next_token`` then there are no more
392+
reports to paginate through.
393+
394+
**Parameters**
395+
396+
The following parameters should be set in the URL:
397+
398+
- ``user_id`` - string - fully qualified: for example, ``@user:server.com``.
399+
- ``limit``: string representing a positive integer - Is optional but is used for pagination,
400+
denoting the maximum number of items to return in this call. Defaults to ``100``.
401+
- ``from``: string representing a positive integer - Is optional but used for pagination,
402+
denoting the offset in the returned results. This should be treated as an opaque value and
403+
not explicitly set to anything other than the return value of ``next_token`` from a previous call.
404+
Defaults to ``0``.
405+
406+
**Response**
407+
408+
The following fields are returned in the JSON response body:
409+
410+
- ``media`` - An array of objects, each containing information about a media.
411+
Media objects contain the following fields:
412+
413+
- ``created_ts`` - integer - Timestamp when the content was uploaded in ms.
414+
- ``last_access_ts`` - integer - Timestamp when the content was last accessed in ms.
415+
- ``media_id`` - string - The id used to refer to the media.
416+
- ``media_length`` - integer - Length of the media in bytes.
417+
- ``media_type`` - string - The MIME-type of the media.
418+
- ``quarantined_by`` - string - The user ID that initiated the quarantine request
419+
for this media.
420+
421+
- ``safe_from_quarantine`` - bool - Status if this media is safe from quarantining.
422+
- ``upload_name`` - string - The name the media was uploaded with.
423+
424+
- ``next_token``: integer - Indication for pagination. See above.
425+
- ``total`` - integer - Total number of media.
426+
344427
User devices
345428
============
346429

synapse/rest/admin/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
ResetPasswordRestServlet,
5454
SearchUsersRestServlet,
5555
UserAdminServlet,
56+
UserMediaRestServlet,
5657
UserMembershipRestServlet,
5758
UserRegisterServlet,
5859
UserRestServletV2,
@@ -218,6 +219,7 @@ def register_servlets(hs, http_server):
218219
SendServerNoticeServlet(hs).register(http_server)
219220
VersionServlet(hs).register(http_server)
220221
UserAdminServlet(hs).register(http_server)
222+
UserMediaRestServlet(hs).register(http_server)
221223
UserMembershipRestServlet(hs).register(http_server)
222224
UserRestServletV2(hs).register(http_server)
223225
UsersRestServletV2(hs).register(http_server)

synapse/rest/admin/users.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import hmac
1717
import logging
1818
from http import HTTPStatus
19+
from typing import Tuple
1920

2021
from synapse.api.constants import UserTypes
2122
from synapse.api.errors import Codes, NotFoundError, SynapseError
@@ -27,13 +28,14 @@
2728
parse_json_object_from_request,
2829
parse_string,
2930
)
31+
from synapse.http.site import SynapseRequest
3032
from synapse.rest.admin._base import (
3133
admin_patterns,
3234
assert_requester_is_admin,
3335
assert_user_is_admin,
3436
historical_admin_path_patterns,
3537
)
36-
from synapse.types import UserID
38+
from synapse.types import JsonDict, UserID
3739

3840
logger = logging.getLogger(__name__)
3941

@@ -709,3 +711,66 @@ async def on_GET(self, request, user_id):
709711
room_ids = await self.store.get_rooms_for_user(user_id)
710712
ret = {"joined_rooms": list(room_ids), "total": len(room_ids)}
711713
return 200, ret
714+
715+
716+
class UserMediaRestServlet(RestServlet):
717+
"""
718+
Gets information about all uploaded local media for a specific `user_id`.
719+
720+
Example:
721+
http://localhost:8008/_synapse/admin/v1/users/
722+
@user:server/media
723+
724+
Args:
725+
The parameters `from` and `limit` are required for pagination.
726+
By default, a `limit` of 100 is used.
727+
Returns:
728+
A list of media and an integer representing the total number of
729+
media that exist given for this user
730+
"""
731+
732+
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]+)/media$")
733+
734+
def __init__(self, hs):
735+
self.is_mine = hs.is_mine
736+
self.auth = hs.get_auth()
737+
self.store = hs.get_datastore()
738+
739+
async def on_GET(
740+
self, request: SynapseRequest, user_id: str
741+
) -> Tuple[int, JsonDict]:
742+
await assert_requester_is_admin(self.auth, request)
743+
744+
if not self.is_mine(UserID.from_string(user_id)):
745+
raise SynapseError(400, "Can only lookup local users")
746+
747+
user = await self.store.get_user_by_id(user_id)
748+
if user is None:
749+
raise NotFoundError("Unknown user")
750+
751+
start = parse_integer(request, "from", default=0)
752+
limit = parse_integer(request, "limit", default=100)
753+
754+
if start < 0:
755+
raise SynapseError(
756+
400,
757+
"Query parameter from must be a string representing a positive integer.",
758+
errcode=Codes.INVALID_PARAM,
759+
)
760+
761+
if limit < 0:
762+
raise SynapseError(
763+
400,
764+
"Query parameter limit must be a string representing a positive integer.",
765+
errcode=Codes.INVALID_PARAM,
766+
)
767+
768+
media, total = await self.store.get_local_media_by_user_paginate(
769+
start, limit, user_id
770+
)
771+
772+
ret = {"media": media, "total": total}
773+
if (start + limit) < total:
774+
ret["next_token"] = start + len(media)
775+
776+
return 200, ret

synapse/storage/databases/main/events_bg_updates.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ def __init__(self, database: DatabasePool, db_conn, hs):
9292
where_clause="NOT have_censored",
9393
)
9494

95+
self.db_pool.updates.register_background_index_update(
96+
"users_have_local_media",
97+
index_name="users_have_local_media",
98+
table="local_media_repository",
99+
columns=["user_id", "created_ts"],
100+
)
101+
95102
async def _background_reindex_fields_sender(self, progress, batch_size):
96103
target_min_stream_id = progress["target_min_stream_id_inclusive"]
97104
max_stream_id = progress["max_stream_id_exclusive"]

synapse/storage/databases/main/media_repository.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,57 @@ async def get_local_media(self, media_id: str) -> Optional[Dict[str, Any]]:
116116
desc="get_local_media",
117117
)
118118

119+
async def get_local_media_by_user_paginate(
120+
self, start: int, limit: int, user_id: str
121+
) -> Tuple[List[Dict[str, Any]], int]:
122+
"""Get a paginated list of metadata for a local piece of media
123+
which an user_id has uploaded
124+
125+
Args:
126+
start: offset in the list
127+
limit: maximum amount of media_ids to retrieve
128+
user_id: fully-qualified user id
129+
Returns:
130+
A paginated list of all metadata of user's media,
131+
plus the total count of all the user's media
132+
"""
133+
134+
def get_local_media_by_user_paginate_txn(txn):
135+
136+
args = [user_id]
137+
sql = """
138+
SELECT COUNT(*) as total_media
139+
FROM local_media_repository
140+
WHERE user_id = ?
141+
"""
142+
txn.execute(sql, args)
143+
count = txn.fetchone()[0]
144+
145+
sql = """
146+
SELECT
147+
"media_id",
148+
"media_type",
149+
"media_length",
150+
"upload_name",
151+
"created_ts",
152+
"last_access_ts",
153+
"quarantined_by",
154+
"safe_from_quarantine"
155+
FROM local_media_repository
156+
WHERE user_id = ?
157+
ORDER BY created_ts DESC, media_id DESC
158+
LIMIT ? OFFSET ?
159+
"""
160+
161+
args += [limit, start]
162+
txn.execute(sql, args)
163+
media = self.db_pool.cursor_to_dict(txn)
164+
return media, count
165+
166+
return await self.db_pool.runInteraction(
167+
"get_local_media_by_user_paginate_txn", get_local_media_by_user_paginate_txn
168+
)
169+
119170
async def get_local_media_before(
120171
self, before_ts: int, size_gt: int, keep_profiles: bool,
121172
) -> Optional[List[str]]:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
INSERT INTO background_updates (update_name, progress_json) VALUES
2+
('users_have_local_media', '{}');

0 commit comments

Comments
 (0)