Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some useful endpoints to Admin API #17948

Merged
merged 47 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a6ae7b7
add functionality to fetch event reports against a specific user
H-Shay Nov 19, 2024
bc8930d
tests
H-Shay Nov 19, 2024
106cb4f
add admin apis to get invite count and joined room count for last 24h…
H-Shay Nov 20, 2024
3c6e281
docs
H-Shay Nov 20, 2024
81f8ce8
newsfragment
H-Shay Nov 20, 2024
d7a6a8c
fix postgres quotes situation
H-Shay Nov 20, 2024
408e1de
should be users
H-Shay Nov 20, 2024
0a1f5af
expand event reports API to filter for the sender of a reported event
H-Shay Nov 21, 2024
121f7d4
extend admin api to get joined rooms to filter via optional timestamp
H-Shay Nov 21, 2024
cda34bb
misc cleanup + add an index on events.received_ts
H-Shay Nov 21, 2024
e2427fd
clarify timestamp
H-Shay Nov 21, 2024
dbd120e
revise docs
H-Shay Dec 2, 2024
4af0387
requested changes
H-Shay Dec 2, 2024
04a3ab7
update tests to reflect changes
H-Shay Dec 2, 2024
a4d2075
Merge branch 'develop' into shay/admin_improvements
H-Shay Dec 2, 2024
69029a9
docs update
H-Shay Dec 3, 2024
40da1b4
docstring update
H-Shay Dec 3, 2024
574e5c4
clean up quesries
H-Shay Dec 3, 2024
52d2905
use set from beginning
H-Shay Dec 3, 2024
227d312
Merge branch 'shay/admin_improvements' of https://github.com/H-Shay/h…
H-Shay Dec 3, 2024
056d49d
requested changes
H-Shay Dec 3, 2024
5140082
update changelog
H-Shay Dec 3, 2024
b651ed8
doc rewording
H-Shay Dec 3, 2024
dc15f58
fix zero check
H-Shay Dec 3, 2024
9e7453f
clean up querys/docstrings
H-Shay Dec 3, 2024
af2b99f
use correct delta
H-Shay Dec 3, 2024
867669a
add test descriptions and separate test concerns
H-Shay Dec 3, 2024
d754849
lint
H-Shay Dec 3, 2024
347e4f7
rescind USING shorthand as it made postgres unhappy
H-Shay Dec 3, 2024
a5762b3
lint
H-Shay Dec 3, 2024
b4ec5c2
clarify terms
H-Shay Dec 4, 2024
2641961
rework queries
H-Shay Dec 4, 2024
e62a1d9
clean up tests
H-Shay Dec 4, 2024
8f73f15
fix tests
H-Shay Dec 4, 2024
d5ad780
use new endpoint
H-Shay Dec 9, 2024
b3ff802
remove unused constants
H-Shay Dec 9, 2024
0132dd1
Merge branch 'develop' into shay/admin_improvements
H-Shay Dec 9, 2024
312dbf1
match ts on > rather than >=
H-Shay Dec 10, 2024
197ed96
clean up tests
H-Shay Dec 10, 2024
6b2c3e3
Merge branch 'shay/admin_improvements' of https://github.com/H-Shay/h…
H-Shay Dec 10, 2024
fc9e4ea
update docs
H-Shay Dec 10, 2024
e9662b9
Merge branch 'develop' into shay/admin_improvements
H-Shay Dec 10, 2024
9b4bad5
match on `>=`
H-Shay Dec 11, 2024
b8f7b90
frozenset + test cleanup
H-Shay Dec 11, 2024
bdfe1b0
Merge branch 'shay/admin_improvements' of https://github.com/H-Shay/h…
H-Shay Dec 11, 2024
288a21e
Merge branch 'develop' into shay/admin_improvements
H-Shay Dec 11, 2024
092e5db
fix changelog wording
H-Shay Dec 16, 2024
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
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).
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry that the review has been a bit stochastic and I wasn't able to pick up on all of these pieces in a single blast.

You might also want to try these queries on matrix.org to see if they run fast enough for you. Having to match on sender without an index might be more detrimental than we expect.

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

MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
* `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.
H-Shay marked this conversation as resolved.
Show resolved Hide resolved

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(
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
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)
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
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
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
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
Loading