Skip to content

Commit

Permalink
Allow knocking rooms (#3647)
Browse files Browse the repository at this point in the history
Signed-off-by: Charly Nguyen <[email protected]>
  • Loading branch information
charlynguyen authored Aug 3, 2023
1 parent 61c0a49 commit 2ef7ae7
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 1 deletion.
80 changes: 79 additions & 1 deletion spec/integ/matrix-client-methods.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down
12 changes: 12 additions & 0 deletions src/@types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
29 changes: 29 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import {
ITagsResponse,
IStatusResponse,
IAddThreePidBody,
KnockRoomOpts,
} from "./@types/requests";
import {
EventType,
Expand Down Expand Up @@ -4158,6 +4159,34 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return syncRoom;
}

/**
* Knock a room. If you have already knocked the room, this will no-op.
* @param roomIdOrAlias - The room ID or room alias to knock.
* @param opts - Options when knocking the room.
* @returns Promise which resolves: `{room_id: {string}}`
* @returns Rejects: with an error response.
*/
public knockRoom(roomIdOrAlias: string, opts: KnockRoomOpts = {}): Promise<{ room_id: string }> {
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<string, string | string[]> = {};
if (opts.viaServers) {
queryParams.server_name = opts.viaServers;
}

const body: Record<string, string> = {};
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.
Expand Down

0 comments on commit 2ef7ae7

Please sign in to comment.