Skip to content

Commit

Permalink
Add some useful endpoints to Admin API (#17948)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
H-Shay authored Dec 16, 2024
1 parent 29d5863 commit 8208186
Show file tree
Hide file tree
Showing 12 changed files with 535 additions and 15 deletions.
3 changes: 3 additions & 0 deletions changelog.d/17948.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add endpoints to Admin API to 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, and get report IDs of event
reports against a provided user (ie where the user was the sender of the reported event).
9 changes: 5 additions & 4 deletions docs/admin_api/event_reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ paginate through.
anything other than the return value of `next_token` from a previous call. Defaults to `0`.
* `dir`: string - Direction of event report order. Whether to fetch the most recent
first (`b`) or the oldest first (`f`). Defaults to `b`.
* `user_id`: string - Is optional and filters to only return users with user IDs that
contain this value. This is the user who reported the event and wrote the reason.
* `room_id`: string - Is optional and filters to only return rooms with room IDs that
contain this value.
* `user_id`: optional string - Filter by the user ID of the reporter. This is the user who reported the event
and wrote the reason.
* `room_id`: optional string - Filter by room id.
* `event_sender_user_id`: optional string - Filter by the sender of the reported event. This is the user who
the report was made against.

**Response**

Expand Down
75 changes: 72 additions & 3 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,9 @@ with a body of:
}
```

## List room memberships of a user
## List joined rooms of a user

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

The API is:

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

## Get the number of invites sent by the user

Fetches the number of invites sent by the provided user ID across all rooms
after the given timestamp.

```
GET /_synapse/admin/v1/users/$user_id/sent_invite_count
```

**Parameters**

The following parameters should be set in the URL:

* `user_id`: fully qualified: for example, `@user:server.com`

The following should be set as query parameters in the URL:

* `from_ts`: int, required. A timestamp in ms from the unix epoch. Only
invites sent at or after the provided timestamp will be returned.
This works by comparing the provided timestamp to the `received_ts`
column in the `events` table.
Note: https://currentmillis.com/ is a useful tool for converting dates
into timestamps and vice versa.

A response body like the following is returned:

```json
{
"invite_count": 30
}
```

_Added in Synapse 1.122.0_

## Get the cumulative number of rooms a user has joined after a given timestamp

Fetches the number of rooms that the user joined after the given timestamp, even
if they have subsequently left/been banned from those rooms.

```
GET /_synapse/admin/v1/users/$<user_id/cumulative_joined_room_count
```

**Parameters**

The following parameters should be set in the URL:

* `user_id`: fully qualified: for example, `@user:server.com`

The following should be set as query parameters in the URL:

* `from_ts`: int, required. A timestamp in ms from the unix epoch. Only
invites sent at or after the provided timestamp will be returned.
This works by comparing the provided timestamp to the `received_ts`
column in the `events` table.
Note: https://currentmillis.com/ is a useful tool for converting dates
into timestamps and vice versa.

A response body like the following is returned:

```json
{
"cumulative_joined_room_count": 30
}
```
_Added in Synapse 1.122.0_

## Account Data
Gets information about account data for a specific `user_id`.

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

_Added in Synapse 1.116.0._
_Added in Synapse 1.116.0._


4 changes: 4 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
UserAdminServlet,
UserByExternalId,
UserByThreePid,
UserInvitesCount,
UserJoinedRoomCount,
UserMembershipRestServlet,
UserRegisterServlet,
UserReplaceMasterCrossSigningKeyRestServlet,
Expand Down Expand Up @@ -323,6 +325,8 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
UserByThreePid(hs).register(http_server)
RedactUser(hs).register(http_server)
RedactUserStatus(hs).register(http_server)
UserInvitesCount(hs).register(http_server)
UserJoinedRoomCount(hs).register(http_server)

DeviceRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
Expand Down
9 changes: 6 additions & 3 deletions synapse/rest/admin/event_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ class EventReportsRestServlet(RestServlet):
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.
The `user_id` query parameter filters by the user ID of the reporter of the event.
The `room_id` query parameter filters by room id.
The `event_sender_user_id` query parameter can be used to filter by the user id
of the sender of the reported event.
Returns:
A list of reported events and an integer representing the total number of
reported events that exist given this query
Expand All @@ -71,6 +73,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
direction = parse_enum(request, "dir", Direction, Direction.BACKWARDS)
user_id = parse_string(request, "user_id")
room_id = parse_string(request, "room_id")
event_sender_user_id = parse_string(request, "event_sender_user_id")

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

event_reports, total = await self._store.get_event_reports_paginate(
start, limit, direction, user_id, room_id
start, limit, direction, user_id, room_id, event_sender_user_id
)
ret = {"event_reports": event_reports, "total": total}
if (start + limit) < total:
Expand Down
54 changes: 51 additions & 3 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,7 @@ async def on_PUT(

class UserMembershipRestServlet(RestServlet):
"""
Get room list of an user.
Get list of joined room ID's for a user.
"""

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

room_ids = await self.store.get_rooms_for_user(user_id)
ret = {"joined_rooms": list(room_ids), "total": len(room_ids)}
return HTTPStatus.OK, ret
rooms_response = {"joined_rooms": list(room_ids), "total": len(room_ids)}

return HTTPStatus.OK, rooms_response


class PushersRestServlet(RestServlet):
Expand Down Expand Up @@ -1501,3 +1502,50 @@ async def on_GET(
}
else:
raise NotFoundError("redact id '%s' not found" % redact_id)


class UserInvitesCount(RestServlet):
"""
Return the count of invites that the user has sent after the given timestamp
"""

PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/sent_invite_count")

def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self.store = hs.get_datastores().main

async def on_GET(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self._auth, request)
from_ts = parse_integer(request, "from_ts", required=True)

sent_invite_count = await self.store.get_sent_invite_count_by_user(
user_id, from_ts
)

return HTTPStatus.OK, {"invite_count": sent_invite_count}


class UserJoinedRoomCount(RestServlet):
"""
Return the count of rooms that the user has joined at or after the given timestamp, even
if they have subsequently left/been banned from those rooms.
"""

PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/cumulative_joined_room_count")

def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self.store = hs.get_datastores().main

async def on_GET(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self._auth, request)
from_ts = parse_integer(request, "from_ts", required=True)

joined_rooms = await self.store.get_rooms_for_user_by_date(user_id, from_ts)

return HTTPStatus.OK, {"cumulative_joined_room_count": len(joined_rooms)}
48 changes: 48 additions & 0 deletions synapse/storage/databases/main/events_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@ def get_chain_id_txn(txn: Cursor) -> int:
writers=["master"],
)

# Added to accommodate some queries for the admin API in order to fetch/filter
# membership events by when it was received
self.db_pool.updates.register_background_index_update(
update_name="events_received_ts_index",
index_name="received_ts_idx",
table="events",
columns=("received_ts",),
where_clause="type = 'm.room.member'",
)

def get_un_partial_stated_events_token(self, instance_name: str) -> int:
return (
self._un_partial_stated_events_stream_id_gen.get_current_token_for_writer(
Expand Down Expand Up @@ -2589,6 +2599,44 @@ async def have_finished_sliding_sync_background_jobs(self) -> bool:
)
)

async def get_sent_invite_count_by_user(self, user_id: str, from_ts: int) -> int:
"""
Get the number of invites sent by the given user at or after the provided timestamp.
Args:
user_id: user ID to search against
from_ts: a timestamp in milliseconds from the unix epoch. Filters against
`events.received_ts`
"""

def _get_sent_invite_count_by_user_txn(
txn: LoggingTransaction, user_id: str, from_ts: int
) -> int:
sql = """
SELECT COUNT(rm.event_id)
FROM room_memberships AS rm
INNER JOIN events AS e USING(event_id)
WHERE rm.sender = ?
AND rm.membership = 'invite'
AND e.type = 'm.room.member'
AND e.received_ts >= ?
"""

txn.execute(sql, (user_id, from_ts))
res = txn.fetchone()

if res is None:
return 0
return int(res[0])

return await self.db_pool.runInteraction(
"_get_sent_invite_count_by_user_txn",
_get_sent_invite_count_by_user_txn,
user_id,
from_ts,
)

@cached(tree=True)
async def get_metadata_for_event(
self, room_id: str, event_id: str
Expand Down
11 changes: 9 additions & 2 deletions synapse/storage/databases/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,7 @@ async def get_event_reports_paginate(
direction: Direction = Direction.BACKWARDS,
user_id: Optional[str] = None,
room_id: Optional[str] = None,
event_sender_user_id: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
"""Retrieve a paginated list of event reports
Expand All @@ -1596,6 +1597,8 @@ async def get_event_reports_paginate(
oldest first (forwards)
user_id: search for user_id. Ignored if user_id is None
room_id: search for room_id. Ignored if room_id is None
event_sender_user_id: search for the sender of the reported event. Ignored if
event_sender_user_id is None
Returns:
Tuple of:
json list of event reports
Expand All @@ -1615,6 +1618,10 @@ def _get_event_reports_paginate_txn(
filters.append("er.room_id LIKE ?")
args.extend(["%" + room_id + "%"])

if event_sender_user_id:
filters.append("events.sender = ?")
args.extend([event_sender_user_id])

if direction == Direction.BACKWARDS:
order = "DESC"
else:
Expand All @@ -1630,6 +1637,7 @@ def _get_event_reports_paginate_txn(
sql = """
SELECT COUNT(*) as total_event_reports
FROM event_reports AS er
LEFT JOIN events USING(event_id)
JOIN room_stats_state ON room_stats_state.room_id = er.room_id
{}
""".format(where_clause)
Expand All @@ -1648,8 +1656,7 @@ def _get_event_reports_paginate_txn(
room_stats_state.canonical_alias,
room_stats_state.name
FROM event_reports AS er
LEFT JOIN events
ON events.event_id = er.event_id
LEFT JOIN events USING(event_id)
JOIN room_stats_state
ON room_stats_state.room_id = er.room_id
{where_clause}
Expand Down
34 changes: 34 additions & 0 deletions synapse/storage/databases/main/roommember.py
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,40 @@ def get_sliding_sync_room_for_user_batch_txn(
get_sliding_sync_room_for_user_batch_txn,
)

async def get_rooms_for_user_by_date(
self, user_id: str, from_ts: int
) -> FrozenSet[str]:
"""
Fetch a list of rooms that the user has joined at or after the given timestamp, including
those they subsequently have left/been banned from.
Args:
user_id: user ID of the user to search for
from_ts: a timestamp in ms from the unix epoch at which to begin the search at
"""

def _get_rooms_for_user_by_join_date_txn(
txn: LoggingTransaction, user_id: str, timestamp: int
) -> frozenset:
sql = """
SELECT rm.room_id
FROM room_memberships AS rm
INNER JOIN events AS e USING (event_id)
WHERE rm.user_id = ?
AND rm.membership = 'join'
AND e.type = 'm.room.member'
AND e.received_ts >= ?
"""
txn.execute(sql, (user_id, timestamp))
return frozenset([r[0] for r in txn])

return await self.db_pool.runInteraction(
"_get_rooms_for_user_by_join_date_txn",
_get_rooms_for_user_by_join_date_txn,
user_id,
from_ts,
)


class RoomMemberBackgroundUpdateStore(SQLBaseStore):
def __init__(
Expand Down
Loading

0 comments on commit 8208186

Please sign in to comment.