From 1a5af9d8e33282896669160dba3a8cc98cfe2cc8 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 5 Jun 2023 10:23:44 +0200 Subject: [PATCH] Update Mutual Rooms (MSC2666) support (#3381) * update mutual rooms support * clarify docs and switch eslint comment with todo * please the holy linter * change query variable names around * add mock tests and fix issue * ye holy linter --- spec/unit/room-member.spec.ts | 134 +++++++++++++++++++++++++++++++++- src/client.ts | 78 ++++++++++++++++---- 2 files changed, 197 insertions(+), 15 deletions(-) diff --git a/spec/unit/room-member.spec.ts b/spec/unit/room-member.spec.ts index bc8a27344..99a2d5895 100644 --- a/spec/unit/room-member.spec.ts +++ b/spec/unit/room-member.spec.ts @@ -14,9 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +import fetchMock from "fetch-mock-jest"; + import * as utils from "../test-utils/test-utils"; import { RoomMember, RoomMemberEvent } from "../../src/models/room-member"; -import { EventType, RoomState } from "../../src"; +import { + createClient, + EventType, + MatrixClient, + RoomState, + UNSTABLE_MSC2666_MUTUAL_ROOMS, + UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS, + UNSTABLE_MSC2666_SHARED_ROOMS, +} from "../../src"; describe("RoomMember", function () { const roomId = "!foo:bar"; @@ -481,3 +491,125 @@ describe("RoomMember", function () { }); }); }); + +describe("MutualRooms", () => { + let client: MatrixClient; + const HS_URL = "https://example.com"; + const TEST_USER_ID = "@alice:localhost"; + const TEST_DEVICE_ID = "xzcvb"; + const QUERIED_USER = "@user:example.com"; + + beforeEach(async () => { + // anything that we don't have a specific matcher for silently returns a 404 + fetchMock.catch(404); + fetchMock.config.warnOnFallback = true; + + client = createClient({ + baseUrl: HS_URL, + userId: TEST_USER_ID, + accessToken: "akjgkrgjs", + deviceId: TEST_DEVICE_ID, + }); + }); + + afterEach(async () => { + await client.stopClient(); + fetchMock.mockReset(); + }); + + function enableFeature(feature: string) { + const mapping: Record = {}; + + mapping[feature] = true; + + fetchMock.get(`${HS_URL}/_matrix/client/versions`, { + unstable_features: mapping, + versions: ["v1.1"], + }); + } + + it("supports the initial MSC version (shared rooms)", async () => { + enableFeature(UNSTABLE_MSC2666_SHARED_ROOMS); + + fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/shared_rooms/:user_id", (rawUrl) => { + const segments = rawUrl.split("/"); + const lastSegment = decodeURIComponent(segments[segments.length - 1]); + + expect(lastSegment).toEqual(QUERIED_USER); + + return { + joined: ["!test:example.com"], + }; + }); + + const rooms = await client._unstable_getSharedRooms(QUERIED_USER); + + expect(rooms).toEqual(["!test:example.com"]); + }); + + it("supports the renaming MSC version (mutual rooms)", async () => { + enableFeature(UNSTABLE_MSC2666_MUTUAL_ROOMS); + + fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms/:user_id", (rawUrl) => { + const segments = rawUrl.split("/"); + const lastSegment = decodeURIComponent(segments[segments.length - 1]); + + expect(lastSegment).toEqual(QUERIED_USER); + + return { + joined: ["!test2:example.com"], + }; + }); + + const rooms = await client._unstable_getSharedRooms(QUERIED_USER); + + expect(rooms).toEqual(["!test2:example.com"]); + }); + + describe("can work the latest MSC version (query mutual rooms)", () => { + beforeEach(() => { + enableFeature(UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS); + }); + + it("works with a simple response", async () => { + fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms", (rawUrl) => { + const url = new URL(rawUrl); + + expect(url.searchParams.get("user_id")).toEqual(QUERIED_USER); + + return { + joined: ["!test3:example.com"], + }; + }); + + const rooms = await client._unstable_getSharedRooms(QUERIED_USER); + + expect(rooms).toEqual(["!test3:example.com"]); + }); + + it("works with a paginated response", async () => { + fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms", (rawUrl) => { + const url = new URL(rawUrl); + + expect(url.searchParams.get("user_id")).toEqual(QUERIED_USER); + + const token = url.searchParams.get("batch_token"); + + if (token == "yahaha") { + return { + joined: ["!korok:example.com"], + }; + } else { + return { + joined: ["!rock:example.com"], + next_batch_token: "yahaha", + }; + } + }); + + const rooms = await client._unstable_getSharedRooms(QUERIED_USER); + + expect(rooms).toEqual(["!rock:example.com", "!korok:example.com"]); + }); + }); +}); diff --git a/src/client.ts b/src/client.ts index 335185054..caca1a89a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -500,6 +500,10 @@ export interface IMSC3882GetLoginTokenCapability extends ICapability {} export const UNSTABLE_MSC3882_CAPABILITY = new UnstableValue("m.get_login_token", "org.matrix.msc3882.get_login_token"); +export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666"; +export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms"; +export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms"; + /** * A representation of the capabilities advertised by a homeserver as defined by * [Capabilities negotiation](https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3capabilities). @@ -7148,29 +7152,75 @@ export class MatrixClient extends TypedEventEmitter { - const sharedRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"); - const mutualRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666.mutual_rooms"); + // Initial variant of the MSC + const sharedRoomsSupport = await this.doesServerSupportUnstableFeature(UNSTABLE_MSC2666_SHARED_ROOMS); - if (!sharedRoomsSupport && !mutualRoomsSupport) { - throw Error("Server does not support mutual_rooms API"); - } + // Newer variant that renamed shared rooms to mutual rooms + const mutualRoomsSupport = await this.doesServerSupportUnstableFeature(UNSTABLE_MSC2666_MUTUAL_ROOMS); - const path = utils.encodeUri( - `/uk.half-shot.msc2666/user/${mutualRoomsSupport ? "mutual_rooms" : "shared_rooms"}/$userId`, - { $userId: userId }, + // Latest variant that changed from path elements to query elements + const queryMutualRoomsSupport = await this.doesServerSupportUnstableFeature( + UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS, ); - const res = await this.http.authedRequest<{ joined: string[] }>(Method.Get, path, undefined, undefined, { - prefix: ClientPrefix.Unstable, - }); - return res.joined; + if (!sharedRoomsSupport && !mutualRoomsSupport && !queryMutualRoomsSupport) { + throw Error("Server does not support the Mutual Rooms API"); + } + + let path; + let query; + + // Cascading unstable support switching. + if (queryMutualRoomsSupport) { + path = "/uk.half-shot.msc2666/user/mutual_rooms"; + query = { user_id: userId }; + } else { + path = utils.encodeUri( + `/uk.half-shot.msc2666/user/${mutualRoomsSupport ? "mutual_rooms" : "shared_rooms"}/$userId`, + { $userId: userId }, + ); + query = {}; + } + + // Accumulated rooms + const rooms: string[] = []; + let token = null; + + do { + const tokenQuery: Record = {}; + if (token != null && queryMutualRoomsSupport) { + tokenQuery["batch_token"] = token; + } + + const res = await this.http.authedRequest<{ + joined: string[]; + next_batch_token?: string; + }>(Method.Get, path, { ...query, ...tokenQuery }, undefined, { + prefix: ClientPrefix.Unstable, + }); + + rooms.push(...res.joined); + + if (res.next_batch_token !== undefined) { + token = res.next_batch_token; + } else { + token = null; + } + } while (token != null); + + return rooms; } /**