Skip to content
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

Allow knocking rooms #3647

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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