Skip to content

Commit 8208186

Browse files
authored
Add some useful endpoints to Admin API (#17948)
- Fetch the number of invites the provided user has sent after a given timestamp - Fetch the number of rooms the provided user has joined after a given timestamp, regardless if they have left/been banned from the rooms subsequently - Get report IDs of event reports where the provided user was the sender of the reported event
1 parent 29d5863 commit 8208186

File tree

12 files changed

+535
-15
lines changed

12 files changed

+535
-15
lines changed

changelog.d/17948.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add endpoints to Admin API to fetch the number of invites the provided user has sent after a given timestamp,
2+
fetch the number of rooms the provided user has joined after a given timestamp, and get report IDs of event
3+
reports against a provided user (ie where the user was the sender of the reported event).

docs/admin_api/event_reports.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ paginate through.
6060
anything other than the return value of `next_token` from a previous call. Defaults to `0`.
6161
* `dir`: string - Direction of event report order. Whether to fetch the most recent
6262
first (`b`) or the oldest first (`f`). Defaults to `b`.
63-
* `user_id`: string - Is optional and filters to only return users with user IDs that
64-
contain this value. This is the user who reported the event and wrote the reason.
65-
* `room_id`: string - Is optional and filters to only return rooms with room IDs that
66-
contain this value.
63+
* `user_id`: optional string - Filter by the user ID of the reporter. This is the user who reported the event
64+
and wrote the reason.
65+
* `room_id`: optional string - Filter by room id.
66+
* `event_sender_user_id`: optional string - Filter by the sender of the reported event. This is the user who
67+
the report was made against.
6768

6869
**Response**
6970

docs/admin_api/user_admin_api.md

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,9 @@ with a body of:
477477
}
478478
```
479479

480-
## List room memberships of a user
480+
## List joined rooms of a user
481481

482-
Gets a list of all `room_id` that a specific `user_id` is member.
482+
Gets a list of all `room_id` that a specific `user_id` is joined to and is a member of (participating in).
483483

484484
The API is:
485485

@@ -516,6 +516,73 @@ The following fields are returned in the JSON response body:
516516
- `joined_rooms` - An array of `room_id`.
517517
- `total` - Number of rooms.
518518

519+
## Get the number of invites sent by the user
520+
521+
Fetches the number of invites sent by the provided user ID across all rooms
522+
after the given timestamp.
523+
524+
```
525+
GET /_synapse/admin/v1/users/$user_id/sent_invite_count
526+
```
527+
528+
**Parameters**
529+
530+
The following parameters should be set in the URL:
531+
532+
* `user_id`: fully qualified: for example, `@user:server.com`
533+
534+
The following should be set as query parameters in the URL:
535+
536+
* `from_ts`: int, required. A timestamp in ms from the unix epoch. Only
537+
invites sent at or after the provided timestamp will be returned.
538+
This works by comparing the provided timestamp to the `received_ts`
539+
column in the `events` table.
540+
Note: https://currentmillis.com/ is a useful tool for converting dates
541+
into timestamps and vice versa.
542+
543+
A response body like the following is returned:
544+
545+
```json
546+
{
547+
"invite_count": 30
548+
}
549+
```
550+
551+
_Added in Synapse 1.122.0_
552+
553+
## Get the cumulative number of rooms a user has joined after a given timestamp
554+
555+
Fetches the number of rooms that the user joined after the given timestamp, even
556+
if they have subsequently left/been banned from those rooms.
557+
558+
```
559+
GET /_synapse/admin/v1/users/$<user_id/cumulative_joined_room_count
560+
```
561+
562+
**Parameters**
563+
564+
The following parameters should be set in the URL:
565+
566+
* `user_id`: fully qualified: for example, `@user:server.com`
567+
568+
The following should be set as query parameters in the URL:
569+
570+
* `from_ts`: int, required. A timestamp in ms from the unix epoch. Only
571+
invites sent at or after the provided timestamp will be returned.
572+
This works by comparing the provided timestamp to the `received_ts`
573+
column in the `events` table.
574+
Note: https://currentmillis.com/ is a useful tool for converting dates
575+
into timestamps and vice versa.
576+
577+
A response body like the following is returned:
578+
579+
```json
580+
{
581+
"cumulative_joined_room_count": 30
582+
}
583+
```
584+
_Added in Synapse 1.122.0_
585+
519586
## Account Data
520587
Gets information about account data for a specific `user_id`.
521588

@@ -1444,4 +1511,6 @@ The following fields are returned in the JSON response body:
14441511
- `failed_redactions` - dictionary - the keys of the dict are event ids the process was unable to redact, if any, and the values are
14451512
the corresponding error that caused the redaction to fail
14461513

1447-
_Added in Synapse 1.116.0._
1514+
_Added in Synapse 1.116.0._
1515+
1516+

synapse/rest/admin/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@
107107
UserAdminServlet,
108108
UserByExternalId,
109109
UserByThreePid,
110+
UserInvitesCount,
111+
UserJoinedRoomCount,
110112
UserMembershipRestServlet,
111113
UserRegisterServlet,
112114
UserReplaceMasterCrossSigningKeyRestServlet,
@@ -323,6 +325,8 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
323325
UserByThreePid(hs).register(http_server)
324326
RedactUser(hs).register(http_server)
325327
RedactUserStatus(hs).register(http_server)
328+
UserInvitesCount(hs).register(http_server)
329+
UserJoinedRoomCount(hs).register(http_server)
326330

327331
DeviceRestServlet(hs).register(http_server)
328332
DevicesRestServlet(hs).register(http_server)

synapse/rest/admin/event_reports.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ class EventReportsRestServlet(RestServlet):
5050
The parameters `from` and `limit` are required only for pagination.
5151
By default, a `limit` of 100 is used.
5252
The parameter `dir` can be used to define the order of results.
53-
The parameter `user_id` can be used to filter by user id.
54-
The parameter `room_id` can be used to filter by room id.
53+
The `user_id` query parameter filters by the user ID of the reporter of the event.
54+
The `room_id` query parameter filters by room id.
55+
The `event_sender_user_id` query parameter can be used to filter by the user id
56+
of the sender of the reported event.
5557
Returns:
5658
A list of reported events and an integer representing the total number of
5759
reported events that exist given this query
@@ -71,6 +73,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
7173
direction = parse_enum(request, "dir", Direction, Direction.BACKWARDS)
7274
user_id = parse_string(request, "user_id")
7375
room_id = parse_string(request, "room_id")
76+
event_sender_user_id = parse_string(request, "event_sender_user_id")
7477

7578
if start < 0:
7679
raise SynapseError(
@@ -87,7 +90,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
8790
)
8891

8992
event_reports, total = await self._store.get_event_reports_paginate(
90-
start, limit, direction, user_id, room_id
93+
start, limit, direction, user_id, room_id, event_sender_user_id
9194
)
9295
ret = {"event_reports": event_reports, "total": total}
9396
if (start + limit) < total:

synapse/rest/admin/users.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,7 @@ async def on_PUT(
983983

984984
class UserMembershipRestServlet(RestServlet):
985985
"""
986-
Get room list of an user.
986+
Get list of joined room ID's for a user.
987987
"""
988988

989989
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/joined_rooms$")
@@ -999,8 +999,9 @@ async def on_GET(
999999
await assert_requester_is_admin(self.auth, request)
10001000

10011001
room_ids = await self.store.get_rooms_for_user(user_id)
1002-
ret = {"joined_rooms": list(room_ids), "total": len(room_ids)}
1003-
return HTTPStatus.OK, ret
1002+
rooms_response = {"joined_rooms": list(room_ids), "total": len(room_ids)}
1003+
1004+
return HTTPStatus.OK, rooms_response
10041005

10051006

10061007
class PushersRestServlet(RestServlet):
@@ -1501,3 +1502,50 @@ async def on_GET(
15011502
}
15021503
else:
15031504
raise NotFoundError("redact id '%s' not found" % redact_id)
1505+
1506+
1507+
class UserInvitesCount(RestServlet):
1508+
"""
1509+
Return the count of invites that the user has sent after the given timestamp
1510+
"""
1511+
1512+
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/sent_invite_count")
1513+
1514+
def __init__(self, hs: "HomeServer"):
1515+
self._auth = hs.get_auth()
1516+
self.store = hs.get_datastores().main
1517+
1518+
async def on_GET(
1519+
self, request: SynapseRequest, user_id: str
1520+
) -> Tuple[int, JsonDict]:
1521+
await assert_requester_is_admin(self._auth, request)
1522+
from_ts = parse_integer(request, "from_ts", required=True)
1523+
1524+
sent_invite_count = await self.store.get_sent_invite_count_by_user(
1525+
user_id, from_ts
1526+
)
1527+
1528+
return HTTPStatus.OK, {"invite_count": sent_invite_count}
1529+
1530+
1531+
class UserJoinedRoomCount(RestServlet):
1532+
"""
1533+
Return the count of rooms that the user has joined at or after the given timestamp, even
1534+
if they have subsequently left/been banned from those rooms.
1535+
"""
1536+
1537+
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/cumulative_joined_room_count")
1538+
1539+
def __init__(self, hs: "HomeServer"):
1540+
self._auth = hs.get_auth()
1541+
self.store = hs.get_datastores().main
1542+
1543+
async def on_GET(
1544+
self, request: SynapseRequest, user_id: str
1545+
) -> Tuple[int, JsonDict]:
1546+
await assert_requester_is_admin(self._auth, request)
1547+
from_ts = parse_integer(request, "from_ts", required=True)
1548+
1549+
joined_rooms = await self.store.get_rooms_for_user_by_date(user_id, from_ts)
1550+
1551+
return HTTPStatus.OK, {"cumulative_joined_room_count": len(joined_rooms)}

synapse/storage/databases/main/events_worker.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,16 @@ def get_chain_id_txn(txn: Cursor) -> int:
339339
writers=["master"],
340340
)
341341

342+
# Added to accommodate some queries for the admin API in order to fetch/filter
343+
# membership events by when it was received
344+
self.db_pool.updates.register_background_index_update(
345+
update_name="events_received_ts_index",
346+
index_name="received_ts_idx",
347+
table="events",
348+
columns=("received_ts",),
349+
where_clause="type = 'm.room.member'",
350+
)
351+
342352
def get_un_partial_stated_events_token(self, instance_name: str) -> int:
343353
return (
344354
self._un_partial_stated_events_stream_id_gen.get_current_token_for_writer(
@@ -2589,6 +2599,44 @@ async def have_finished_sliding_sync_background_jobs(self) -> bool:
25892599
)
25902600
)
25912601

2602+
async def get_sent_invite_count_by_user(self, user_id: str, from_ts: int) -> int:
2603+
"""
2604+
Get the number of invites sent by the given user at or after the provided timestamp.
2605+
2606+
Args:
2607+
user_id: user ID to search against
2608+
from_ts: a timestamp in milliseconds from the unix epoch. Filters against
2609+
`events.received_ts`
2610+
2611+
"""
2612+
2613+
def _get_sent_invite_count_by_user_txn(
2614+
txn: LoggingTransaction, user_id: str, from_ts: int
2615+
) -> int:
2616+
sql = """
2617+
SELECT COUNT(rm.event_id)
2618+
FROM room_memberships AS rm
2619+
INNER JOIN events AS e USING(event_id)
2620+
WHERE rm.sender = ?
2621+
AND rm.membership = 'invite'
2622+
AND e.type = 'm.room.member'
2623+
AND e.received_ts >= ?
2624+
"""
2625+
2626+
txn.execute(sql, (user_id, from_ts))
2627+
res = txn.fetchone()
2628+
2629+
if res is None:
2630+
return 0
2631+
return int(res[0])
2632+
2633+
return await self.db_pool.runInteraction(
2634+
"_get_sent_invite_count_by_user_txn",
2635+
_get_sent_invite_count_by_user_txn,
2636+
user_id,
2637+
from_ts,
2638+
)
2639+
25922640
@cached(tree=True)
25932641
async def get_metadata_for_event(
25942642
self, room_id: str, event_id: str

synapse/storage/databases/main/room.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,7 @@ async def get_event_reports_paginate(
15861586
direction: Direction = Direction.BACKWARDS,
15871587
user_id: Optional[str] = None,
15881588
room_id: Optional[str] = None,
1589+
event_sender_user_id: Optional[str] = None,
15891590
) -> Tuple[List[Dict[str, Any]], int]:
15901591
"""Retrieve a paginated list of event reports
15911592
@@ -1596,6 +1597,8 @@ async def get_event_reports_paginate(
15961597
oldest first (forwards)
15971598
user_id: search for user_id. Ignored if user_id is None
15981599
room_id: search for room_id. Ignored if room_id is None
1600+
event_sender_user_id: search for the sender of the reported event. Ignored if
1601+
event_sender_user_id is None
15991602
Returns:
16001603
Tuple of:
16011604
json list of event reports
@@ -1615,6 +1618,10 @@ def _get_event_reports_paginate_txn(
16151618
filters.append("er.room_id LIKE ?")
16161619
args.extend(["%" + room_id + "%"])
16171620

1621+
if event_sender_user_id:
1622+
filters.append("events.sender = ?")
1623+
args.extend([event_sender_user_id])
1624+
16181625
if direction == Direction.BACKWARDS:
16191626
order = "DESC"
16201627
else:
@@ -1630,6 +1637,7 @@ def _get_event_reports_paginate_txn(
16301637
sql = """
16311638
SELECT COUNT(*) as total_event_reports
16321639
FROM event_reports AS er
1640+
LEFT JOIN events USING(event_id)
16331641
JOIN room_stats_state ON room_stats_state.room_id = er.room_id
16341642
{}
16351643
""".format(where_clause)
@@ -1648,8 +1656,7 @@ def _get_event_reports_paginate_txn(
16481656
room_stats_state.canonical_alias,
16491657
room_stats_state.name
16501658
FROM event_reports AS er
1651-
LEFT JOIN events
1652-
ON events.event_id = er.event_id
1659+
LEFT JOIN events USING(event_id)
16531660
JOIN room_stats_state
16541661
ON room_stats_state.room_id = er.room_id
16551662
{where_clause}

synapse/storage/databases/main/roommember.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,40 @@ def get_sliding_sync_room_for_user_batch_txn(
15721572
get_sliding_sync_room_for_user_batch_txn,
15731573
)
15741574

1575+
async def get_rooms_for_user_by_date(
1576+
self, user_id: str, from_ts: int
1577+
) -> FrozenSet[str]:
1578+
"""
1579+
Fetch a list of rooms that the user has joined at or after the given timestamp, including
1580+
those they subsequently have left/been banned from.
1581+
1582+
Args:
1583+
user_id: user ID of the user to search for
1584+
from_ts: a timestamp in ms from the unix epoch at which to begin the search at
1585+
"""
1586+
1587+
def _get_rooms_for_user_by_join_date_txn(
1588+
txn: LoggingTransaction, user_id: str, timestamp: int
1589+
) -> frozenset:
1590+
sql = """
1591+
SELECT rm.room_id
1592+
FROM room_memberships AS rm
1593+
INNER JOIN events AS e USING (event_id)
1594+
WHERE rm.user_id = ?
1595+
AND rm.membership = 'join'
1596+
AND e.type = 'm.room.member'
1597+
AND e.received_ts >= ?
1598+
"""
1599+
txn.execute(sql, (user_id, timestamp))
1600+
return frozenset([r[0] for r in txn])
1601+
1602+
return await self.db_pool.runInteraction(
1603+
"_get_rooms_for_user_by_join_date_txn",
1604+
_get_rooms_for_user_by_join_date_txn,
1605+
user_id,
1606+
from_ts,
1607+
)
1608+
15751609

15761610
class RoomMemberBackgroundUpdateStore(SQLBaseStore):
15771611
def __init__(

0 commit comments

Comments
 (0)