-
Notifications
You must be signed in to change notification settings - Fork 424
Implement pagination for MSC2666 #19279
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
base: develop
Are you sure you want to change the base?
Changes from 2 commits
3ff83ed
2357d38
3b977e1
af072d9
1a779e8
c8eb6fc
bed637f
54a4e07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Implemented pagination for the [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666) mutual rooms endpoint. Contributed by @tulir @ Beeper. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,9 +19,12 @@ | |
| # | ||
| # | ||
| import logging | ||
| from bisect import bisect | ||
| from http import HTTPStatus | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from unpaddedbase64 import decode_base64, encode_base64 | ||
|
|
||
| from synapse.api.errors import Codes, SynapseError | ||
| from synapse.http.server import HttpServer | ||
| from synapse.http.servlet import RestServlet, parse_strings_from_args | ||
|
|
@@ -35,6 +38,30 @@ | |
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| MUTUAL_ROOMS_BATCH_LIMIT = 100 | ||
|
|
||
|
|
||
| def _parse_mutual_rooms_batch_token_args(args: dict[bytes, list[bytes]]) -> str | None: | ||
| from_batches = parse_strings_from_args(args, "from") | ||
| if not from_batches: | ||
| return None | ||
| if len(from_batches) > 1: | ||
| raise SynapseError( | ||
| HTTPStatus.BAD_REQUEST, | ||
| "Duplicate from query parameter", | ||
| errcode=Codes.INVALID_PARAM, | ||
| ) | ||
| if from_batches[0]: | ||
| try: | ||
| return decode_base64(from_batches[0]).decode("utf-8") | ||
| except Exception: | ||
| raise SynapseError( | ||
| HTTPStatus.BAD_REQUEST, | ||
| "Malformed from token", | ||
| errcode=Codes.INVALID_PARAM, | ||
| ) | ||
| return None | ||
|
|
||
|
|
||
| class UserMutualRoomsServlet(RestServlet): | ||
| """ | ||
|
|
@@ -56,6 +83,7 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: | |
| args: dict[bytes, list[bytes]] = request.args # type: ignore | ||
|
|
||
| user_ids = parse_strings_from_args(args, "user_id", required=True) | ||
| from_batch = _parse_mutual_rooms_batch_token_args(args) | ||
|
|
||
| if len(user_ids) > 1: | ||
| raise SynapseError( | ||
|
|
@@ -64,29 +92,44 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: | |
| errcode=Codes.INVALID_PARAM, | ||
| ) | ||
|
|
||
| # We don't do batching, so a batch token is illegal by default | ||
| if b"batch_token" in args: | ||
| raise SynapseError( | ||
| HTTPStatus.BAD_REQUEST, | ||
| "Unknown batch_token", | ||
| errcode=Codes.INVALID_PARAM, | ||
| ) | ||
|
|
||
| user_id = user_ids[0] | ||
|
|
||
| requester = await self.auth.get_user_by_req(request) | ||
| if user_id == requester.user.to_string(): | ||
| raise SynapseError( | ||
| HTTPStatus.UNPROCESSABLE_ENTITY, | ||
| HTTPStatus.BAD_REQUEST, | ||
| "You cannot request a list of shared rooms with yourself", | ||
| errcode=Codes.INVALID_PARAM, | ||
| errcode=Codes.UNKNOWN, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change was made a couple years back on the MSC because 422 is a weird http status that nobody uses |
||
| ) | ||
|
|
||
| rooms = await self.store.get_mutual_rooms_between_users( | ||
| frozenset((requester.user.to_string(), user_id)) | ||
| rooms = sorted( | ||
tulir marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| await self.store.get_mutual_rooms_between_users( | ||
| frozenset((requester.user.to_string(), user_id)) | ||
| ) | ||
| ) | ||
|
|
||
| return 200, {"joined": list(rooms)} | ||
| if from_batch: | ||
| # A from_batch token was provided, so cut off any rooms where the ID is | ||
| # lower than or equal to the token | ||
| rooms = rooms[bisect(rooms, from_batch) :] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that in the case where an invalid I think this is fine, since it's unlikely that someone will provide a valid base64 string that's not the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah it's intentional that the exact value of the token isn't validated. Due to the truncation it's not even guaranteed to be a real room ID, plus even if it is, the room might have disappeared after the first request. |
||
|
|
||
| if len(rooms) < MUTUAL_ROOMS_BATCH_LIMIT: | ||
| # We've reached the end of the list, don't return a batch token | ||
| return 200, {"joined": rooms} | ||
tulir marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| rooms = rooms[:MUTUAL_ROOMS_BATCH_LIMIT] | ||
| # We use urlsafe unpadded base64 encoding for the batch token in order to | ||
| # handle funny room IDs in old pre-v12 rooms properly. We also truncate it | ||
| # to stay within the 255-character limit of opaque tokens. | ||
| next_batch = encode_base64(rooms[-1].encode("utf-8"), urlsafe=True)[:255] | ||
|
Comment on lines
+130
to
+132
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should acknowledge in a comment that this list can and will change underneath the user, and as such may omit or duplicate a room - but that otherwise this is a simple and pragmatic solution.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I updated the comment in the bisect code |
||
| # Due to the truncation, it is technically possible to have conflicting next | ||
| # batches by creating hundreds of rooms with the same 191 character prefix | ||
| # in the room ID. In the event that some silly user does that, don't let | ||
| # them paginate further. | ||
| if next_batch == from_batch: | ||
| return 200, {"joined": rooms} | ||
|
|
||
| return 200, {"joined": list(rooms), "next_batch": next_batch} | ||
|
|
||
|
|
||
| def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're feeling up to it, I would love to see some Complement tests for this endpoint as well. But not a blocker for this PR (or the spec). |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -55,12 +55,16 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: | ||||||||||||||||||||||||||||||||||||||
| self.store = hs.get_datastores().main | ||||||||||||||||||||||||||||||||||||||
| mutual_rooms.MUTUAL_ROOMS_BATCH_LIMIT = 10 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _get_mutual_rooms(self, token: str, other_user: str) -> FakeChannel: | ||||||||||||||||||||||||||||||||||||||
| def _get_mutual_rooms( | ||||||||||||||||||||||||||||||||||||||
| self, token: str, other_user: str, since_token: str | None = None | ||||||||||||||||||||||||||||||||||||||
| ) -> FakeChannel: | ||||||||||||||||||||||||||||||||||||||
| return self.make_request( | ||||||||||||||||||||||||||||||||||||||
| "GET", | ||||||||||||||||||||||||||||||||||||||
| "/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms" | ||||||||||||||||||||||||||||||||||||||
| f"?user_id={quote(other_user)}", | ||||||||||||||||||||||||||||||||||||||
| f"?user_id={quote(other_user)}" | ||||||||||||||||||||||||||||||||||||||
| + (f"&from={quote(since_token)}" if since_token else ""), | ||||||||||||||||||||||||||||||||||||||
| access_token=token, | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -141,6 +145,30 @@ def _check_mutual_rooms_with( | |||||||||||||||||||||||||||||||||||||
| for room_id_id in channel.json_body["joined"]: | ||||||||||||||||||||||||||||||||||||||
| self.assertIn(room_id_id, [room_id_one, room_id_two]) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_shared_room_list_pagination(self) -> None: | ||||||||||||||||||||||||||||||||||||||
| u1 = self.register_user("user1", "pass") | ||||||||||||||||||||||||||||||||||||||
| u1_token = self.login(u1, "pass") | ||||||||||||||||||||||||||||||||||||||
| u2 = self.register_user("user2", "pass") | ||||||||||||||||||||||||||||||||||||||
| u2_token = self.login(u2, "pass") | ||||||||||||||||||||||||||||||||||||||
| self.get_success(self.hs.get_datastores().main.set_ratelimit_for_user(u1, 0, 0)) | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| "rc_message": {"per_second": 10000, "burst_count": 10000}, | |
| "rc_registration": {"per_second": 10000, "burst_count": 10000}, | |
| "rc_login": { | |
| "address": {"per_second": 10000, "burst_count": 10000}, | |
| "account": {"per_second": 10000, "burst_count": 10000}, | |
| "failed_attempts": {"per_second": 10000, "burst_count": 10000}, | |
| }, | |
| "rc_joins": { | |
| "local": {"per_second": 10000, "burst_count": 10000}, | |
| "remote": {"per_second": 10000, "burst_count": 10000}, | |
| }, | |
| "rc_joins_per_room": {"per_second": 10000, "burst_count": 10000}, | |
| "rc_invites": { | |
| "per_room": {"per_second": 10000, "burst_count": 10000}, | |
| "per_user": {"per_second": 10000, "burst_count": 10000}, | |
| }, | |
| "rc_3pid_validation": {"per_second": 10000, "burst_count": 10000}, | |
| "rc_presence": {"per_user": {"per_second": 10000, "burst_count": 10000}}, |
Did you find this was necessary? (Or is it just due to us missing rc_room_creation in that config?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test exploded with a 429 in room creation until I added that, I guess it's the missing rc_room_creation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to add both rc_room_creation and rc_invites->per_issuer to make it pass without the ratelimit override
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add some additional tests for:
- an invalid from token being passed (specifically something that would trigger INVALID_PARAM).
- a batch limit of 10, and a set of 10 mutual rooms. check that next_batch is not provided.
- trying to get mutual rooms with a user that does not exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow this was an exceptionally unhelpful error.