From e874468ba3e84819cf4b342d2e66af67ab4cf804 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 23 Apr 2024 02:39:10 +0900 Subject: [PATCH] Improve compliance with MSC3266 (#4155) * Fix fields of MSC 3266 summary object Also remove redundant room_type field which is inherited from elsewhere * Export the MSC 3266 summary type * Use proper endpoint for MSC 3266 summary lookup Use the endpoint recommended by the MSC * Rename newly-exported symbol to not start with I * Use "export type" * Lint * Fix type of "encryption" field * Add TSDoc documentation * Add basic integration test for getRoomSummary * Lint * Use fallback endpoint for MSC3266 * Improve test coverage * Lint * Refactor async catch to satisfy linter * Increase test coverage --- spec/integ/matrix-client-methods.spec.ts | 107 ++++++++++++++++++++++- src/client.ts | 40 +++++++-- src/matrix.ts | 1 + 3 files changed, 139 insertions(+), 9 deletions(-) diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index ddfdcf350..a66d41b3d 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -19,7 +19,16 @@ import { Mocked } from "jest-mock"; import * as utils from "../test-utils/test-utils"; import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client"; import { MatrixEvent } from "../../src/models/event"; -import { Filter, KnockRoomOpts, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix"; +import { + Filter, + JoinRule, + KnockRoomOpts, + MemoryStore, + Method, + Room, + RoomSummary, + SERVICE_TYPES, +} from "../../src/matrix"; import { TestClient } from "../TestClient"; import { THREAD_RELATION_TYPE } from "../../src/models/thread"; import { IFilterDefinition } from "../../src/filter"; @@ -1710,6 +1719,102 @@ describe("MatrixClient", function () { await Promise.all([client.unbindThreePid("email", "alice@server.com"), httpBackend.flushAllExpected()]); }); }); + + describe("getRoomSummary", () => { + const roomId = "!foo:bar"; + const encodedRoomId = encodeURIComponent(roomId); + + const roomSummary: RoomSummary = { + "room_id": roomId, + "name": "My Room", + "avatar_url": "", + "topic": "My room topic", + "world_readable": false, + "guest_can_join": false, + "num_joined_members": 1, + "room_type": "", + "join_rule": JoinRule.Public, + "membership": "leave", + "im.nheko.summary.room_version": "6", + "im.nheko.summary.encryption": "algo", + }; + + const prefix = "/_matrix/client/unstable/im.nheko.summary/"; + const suffix = `summary/${encodedRoomId}`; + const deprecatedSuffix = `rooms/${encodedRoomId}/summary`; + + const errorUnrecogStatus = 404; + const errorUnrecogBody = { + errcode: "M_UNRECOGNIZED", + error: "Unsupported endpoint", + }; + + const errorBadreqStatus = 400; + const errorBadreqBody = { + errcode: "M_UNKNOWN", + error: "Invalid request", + }; + + it("should respond with a valid room summary object", () => { + httpBackend.when("GET", prefix + suffix).respond(200, roomSummary); + + const prom = client.getRoomSummary(roomId).then((response) => { + expect(response).toEqual(roomSummary); + }); + + httpBackend.flush(""); + return prom; + }); + + it("should allow fallback to the deprecated endpoint", () => { + httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody); + httpBackend.when("GET", prefix + deprecatedSuffix).respond(200, roomSummary); + + const prom = client.getRoomSummary(roomId).then((response) => { + expect(response).toEqual(roomSummary); + }); + + httpBackend.flush(""); + return prom; + }); + + it("should respond to unsupported path with error", () => { + httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody); + httpBackend.when("GET", prefix + deprecatedSuffix).respond(errorUnrecogStatus, errorUnrecogBody); + + const prom = client.getRoomSummary(roomId).then( + function (response) { + throw Error("request not failed"); + }, + function (error) { + expect(error.httpStatus).toEqual(errorUnrecogStatus); + expect(error.errcode).toEqual(errorUnrecogBody.errcode); + expect(error.message).toEqual(`MatrixError: [${errorUnrecogStatus}] ${errorUnrecogBody.error}`); + }, + ); + + httpBackend.flush(""); + return prom; + }); + + it("should respond to invalid path arguments with error", () => { + httpBackend.when("GET", prefix).respond(errorBadreqStatus, errorBadreqBody); + + const prom = client.getRoomSummary("notAroom").then( + function (response) { + throw Error("request not failed"); + }, + function (error) { + expect(error.httpStatus).toEqual(errorBadreqStatus); + expect(error.errcode).toEqual(errorBadreqBody.errcode); + expect(error.message).toEqual(`MatrixError: [${errorBadreqStatus}] ${errorBadreqBody.error}`); + }, + ); + + httpBackend.flush(""); + return prom; + }); + }); }); function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent { diff --git a/src/client.ts b/src/client.ts index 6de5cd5d7..64e6855ea 100644 --- a/src/client.ts +++ b/src/client.ts @@ -865,10 +865,24 @@ interface IThirdPartyUser { fields: object; } -interface IRoomSummary extends Omit { - room_type?: RoomType; - membership?: Membership; - is_encrypted: boolean; +/** + * The summary of a room as defined by an initial version of MSC3266 and implemented in Synapse + * Proposed at https://github.com/matrix-org/matrix-doc/pull/3266 + */ +export interface RoomSummary extends Omit { + /** + * The current membership of this user in the room. + * Usually "leave" if the room is fetched over federation. + */ + "membership"?: Membership; + /** + * Version of the room. + */ + "im.nheko.summary.room_version"?: string; + /** + * The encryption algorithm used for this room, if the room is encrypted. + */ + "im.nheko.summary.encryption"?: string; } interface IRoomKeysResponse { @@ -9786,11 +9800,21 @@ export class MatrixClient extends TypedEventEmitter { - const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias }); - return this.http.authedRequest(Method.Get, path, { via }, undefined, { + public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise { + const paramOpts = { prefix: "/_matrix/client/unstable/im.nheko.summary", - }); + }; + try { + const path = utils.encodeUri("/summary/$roomid", { $roomid: roomIdOrAlias }); + return await this.http.authedRequest(Method.Get, path, { via }, undefined, paramOpts); + } catch (e) { + if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") { + const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias }); + return await this.http.authedRequest(Method.Get, path, { via }, undefined, paramOpts); + } else { + throw e; + } + } } /** diff --git a/src/matrix.ts b/src/matrix.ts index 4e12c9feb..ed71f7dfc 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -77,6 +77,7 @@ export * from "./@types/extensible_events"; export * from "./@types/IIdentityServerProvider"; export * from "./models/room-summary"; export * from "./models/event-status"; +export type { RoomSummary } from "./client"; export * as ContentHelpers from "./content-helpers"; export * as SecretStorage from "./secret-storage"; export type { ICryptoCallbacks } from "./crypto"; // used to be located here