From 2ef7ae7661b9ac036f50819efd2ae2a72ae61b4d Mon Sep 17 00:00:00 2001 From: Charly Nguyen <1422657+charlynguyen@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:16:18 +0200 Subject: [PATCH] Allow knocking rooms (#3647) Signed-off-by: Charly Nguyen --- spec/integ/matrix-client-methods.spec.ts | 80 +++++++++++++++++++++++- src/@types/requests.ts | 12 ++++ src/client.ts | 29 +++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index a44b3815bd5..89f69069a6a 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -19,7 +19,7 @@ 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, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix"; +import { Filter, KnockRoomOpts, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix"; import { TestClient } from "../TestClient"; import { THREAD_RELATION_TYPE } from "../../src/models/thread"; import { IFilterDefinition } from "../../src/filter"; @@ -205,6 +205,84 @@ describe("MatrixClient", function () { }); }); + describe("knockRoom", function () { + const roomId = "!some-room-id:example.org"; + const reason = "some reason"; + const viaServers = "example.com"; + + type TestCase = [string, KnockRoomOpts]; + const testCases: TestCase[] = [ + ["should knock a room", {}], + ["should knock a room for a reason", { reason }], + ["should knock a room via given servers", { viaServers }], + ["should knock a room for a reason via given servers", { reason, viaServers }], + ]; + + it.each(testCases)("%s", async (_, opts) => { + httpBackend + .when("POST", "/knock/" + encodeURIComponent(roomId)) + .check((request) => { + expect(request.data).toEqual({ reason: opts.reason }); + expect(request.queryParams).toEqual({ server_name: opts.viaServers }); + }) + .respond(200, { room_id: roomId }); + + const prom = client.knockRoom(roomId, opts); + await httpBackend.flushAllExpected(); + expect((await prom).room_id).toBe(roomId); + }); + + it("should no-op if you've already knocked a room", function () { + const room = new Room(roomId, client, userId); + + client.fetchRoomEvent = () => + Promise.resolve({ + type: "test", + content: {}, + }); + + room.addLiveEvents([ + utils.mkMembership({ + user: userId, + room: roomId, + mship: "knock", + event: true, + }), + ]); + + httpBackend.verifyNoOutstandingRequests(); + store.storeRoom(room); + client.knockRoom(roomId); + httpBackend.verifyNoOutstandingRequests(); + }); + + describe("errors", function () { + type TestCase = [number, { errcode: string; error?: string }, string]; + const testCases: TestCase[] = [ + [ + 403, + { errcode: "M_FORBIDDEN", error: "You don't have permission to knock" }, + "[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]", + ], + [ + 500, + { errcode: "INTERNAL_SERVER_ERROR" }, + "[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]", + ], + ]; + + it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => { + httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error }); + + const prom = client.knockRoom(roomId); + await Promise.all([ + httpBackend.flushAllExpected(), + expect(prom).rejects.toMatchInlineSnapshot(snapshot), + ]); + }); + }); + }); + describe("getFilter", function () { const filterId = "f1lt3r1d"; diff --git a/src/@types/requests.ts b/src/@types/requests.ts index eca0132fb31..bfab7fa0145 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -45,6 +45,18 @@ export interface IJoinRoomOpts { viaServers?: string[]; } +export interface KnockRoomOpts { + /** + * The reason for the knock. + */ + reason?: string; + + /** + * The server names to try and knock through in addition to those that are automatically chosen. + */ + viaServers?: string | string[]; +} + export interface IRedactOpts { reason?: string; /** diff --git a/src/client.ts b/src/client.ts index 55402e3c631..94f01c173a3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -134,6 +134,7 @@ import { ITagsResponse, IStatusResponse, IAddThreePidBody, + KnockRoomOpts, } from "./@types/requests"; import { EventType, @@ -4158,6 +4159,34 @@ export class MatrixClient extends TypedEventEmitter { + const room = this.getRoom(roomIdOrAlias); + if (room?.hasMembershipState(this.credentials.userId!, "knock")) { + return Promise.resolve({ room_id: room.roomId }); + } + + const path = utils.encodeUri("/knock/$roomIdOrAlias", { $roomIdOrAlias: roomIdOrAlias }); + + const queryParams: Record = {}; + if (opts.viaServers) { + queryParams.server_name = opts.viaServers; + } + + const body: Record = {}; + if (opts.reason) { + body.reason = opts.reason; + } + + return this.http.authedRequest(Method.Post, path, queryParams, body); + } + /** * Resend an event. Will also retry any to-device messages waiting to be sent. * @param event - The event to resend.