You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Sync knock rooms (#3703)
Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> Co-authored-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
This commit is contained in:
@ -223,6 +223,121 @@ describe("MatrixClient syncing", () => {
|
||||
expect(fires).toBe(3);
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
|
||||
await client!.initCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
// First sync: an knock
|
||||
const knockSyncRoomSection = {
|
||||
knock: {
|
||||
[roomId]: {
|
||||
knock_state: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "knock",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: knockSyncRoomSection,
|
||||
});
|
||||
|
||||
// Second sync: a leave (reject of some kind)
|
||||
httpBackend!.when("POST", "/leave").respond(200, {});
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: {
|
||||
leave: {
|
||||
[roomId]: {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
state: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
prev_content: {
|
||||
membership: "knock",
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
limited: false,
|
||||
events: [
|
||||
{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "leave",
|
||||
},
|
||||
prev_content: {
|
||||
membership: "knock",
|
||||
},
|
||||
// XXX: And other fields required on an event
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Third sync: another knock
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: knockSyncRoomSection,
|
||||
});
|
||||
|
||||
// First fire: an initial knock
|
||||
let fires = 0;
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
// Room, string, string
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("knock");
|
||||
expect(oldMembership).toBeFalsy();
|
||||
|
||||
// Second fire: a leave
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("leave");
|
||||
expect(oldMembership).toBe("knock");
|
||||
|
||||
// Third/final fire: a second knock
|
||||
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
expect(membership).toBe("knock");
|
||||
expect(oldMembership).toBe("leave");
|
||||
});
|
||||
});
|
||||
|
||||
// For maximum safety, "leave" the room after we register the handler
|
||||
client!.leave(roomId);
|
||||
});
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
client!.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(fires).toBe(3);
|
||||
});
|
||||
|
||||
it("should honour lazyLoadMembers if user is not a guest", () => {
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
@ -293,6 +408,46 @@ describe("MatrixClient syncing", () => {
|
||||
expect(fires).toBe(1);
|
||||
});
|
||||
|
||||
it("should emit ClientEvent.Room when knocked while crypto is disabled", async () => {
|
||||
const roomId = "!knock:example.org";
|
||||
|
||||
// First sync: a knock
|
||||
const knockSyncRoomSection = {
|
||||
knock: {
|
||||
[roomId]: {
|
||||
knock_state: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.member",
|
||||
state_key: selfUserId,
|
||||
content: {
|
||||
membership: "knock",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
...syncData,
|
||||
rooms: knockSyncRoomSection,
|
||||
});
|
||||
|
||||
// First fire: an initial knock
|
||||
let fires = 0;
|
||||
client!.once(ClientEvent.Room, (room) => {
|
||||
fires++;
|
||||
expect(room.roomId).toBe(roomId);
|
||||
});
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
client!.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(fires).toBe(1);
|
||||
});
|
||||
|
||||
it("should work when all network calls fail", async () => {
|
||||
httpBackend!.expectedRequests = [];
|
||||
httpBackend!.when("GET", "").fail(0, new Error("CORS or something"));
|
||||
@ -358,6 +513,7 @@ describe("MatrixClient syncing", () => {
|
||||
join: {},
|
||||
invite: {},
|
||||
leave: {},
|
||||
knock: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -376,6 +376,7 @@ describe("MatrixClient syncing", () => {
|
||||
},
|
||||
[Category.Leave]: {},
|
||||
[Category.Invite]: {},
|
||||
[Category.Knock]: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -99,6 +99,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
||||
join: { [roomId]: roomResponse },
|
||||
invite: {},
|
||||
leave: {},
|
||||
knock: {},
|
||||
},
|
||||
account_data: { events: [] },
|
||||
};
|
||||
|
@ -16,8 +16,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { IJoinedRoom, ISyncResponse, SyncAccumulator } from "../../src/sync-accumulator";
|
||||
import { IJoinedRoom, IKnockedRoom, IStrippedState, ISyncResponse, SyncAccumulator } from "../../src/sync-accumulator";
|
||||
import { IRoomSummary } from "../../src";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
|
||||
// The event body & unsigned object get frozen to assert that they don't get altered
|
||||
// by the impl
|
||||
@ -95,6 +96,13 @@ describe("SyncAccumulator", function () {
|
||||
},
|
||||
},
|
||||
},
|
||||
knock: {
|
||||
"!knock": {
|
||||
knock_state: {
|
||||
events: [member("alice", "knock")],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
sa.accumulate(res);
|
||||
@ -287,6 +295,71 @@ describe("SyncAccumulator", function () {
|
||||
expect(sa.getJSON().accountData[0]).toEqual(acc2);
|
||||
});
|
||||
|
||||
it("should accumulate knock state", () => {
|
||||
const initKnockState = {
|
||||
events: [member("alice", "knock")],
|
||||
};
|
||||
sa.accumulate(
|
||||
syncSkeleton(
|
||||
{},
|
||||
{
|
||||
knock_state: initKnockState,
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(sa.getJSON().roomsData.knock["!knock:bar"].knock_state).toBe(initKnockState);
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton(
|
||||
{},
|
||||
{
|
||||
knock_state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
user: "alice",
|
||||
room: "!knock:bar",
|
||||
type: "m.room.name",
|
||||
content: {
|
||||
name: "Room 1",
|
||||
},
|
||||
}) as IStrippedState,
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
sa.getJSON().roomsData.knock["!knock:bar"].knock_state.events.find((e) => e.type === "m.room.name")?.content
|
||||
.name,
|
||||
).toEqual("Room 1");
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton(
|
||||
{},
|
||||
{
|
||||
knock_state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
user: "alice",
|
||||
room: "!knock:bar",
|
||||
type: "m.room.name",
|
||||
content: {
|
||||
name: "Room 2",
|
||||
},
|
||||
}) as IStrippedState,
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
sa.getJSON().roomsData.knock["!knock:bar"].knock_state.events.find((e) => e.type === "m.room.name")?.content
|
||||
.name,
|
||||
).toEqual("Room 2");
|
||||
});
|
||||
|
||||
it("should accumulate read receipts", () => {
|
||||
const receipt1 = {
|
||||
type: "m.receipt",
|
||||
@ -601,7 +674,7 @@ describe("SyncAccumulator", function () {
|
||||
});
|
||||
});
|
||||
|
||||
function syncSkeleton(joinObj: Partial<IJoinedRoom>): ISyncResponse {
|
||||
function syncSkeleton(joinObj: Partial<IJoinedRoom>, knockObj?: Partial<IKnockedRoom>): ISyncResponse {
|
||||
joinObj = joinObj || {};
|
||||
return {
|
||||
next_batch: "abc",
|
||||
@ -609,6 +682,11 @@ function syncSkeleton(joinObj: Partial<IJoinedRoom>): ISyncResponse {
|
||||
join: {
|
||||
"!foo:bar": joinObj,
|
||||
},
|
||||
knock: knockObj
|
||||
? {
|
||||
"!knock:bar": knockObj,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
}
|
||||
|
@ -849,7 +849,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the membership type (join | leave | invite) for the logged in user
|
||||
* @returns the membership type (join | leave | invite | knock) for the logged in user
|
||||
*/
|
||||
public getMyMembership(): string {
|
||||
return this.selfMembership ?? "leave";
|
||||
|
@ -99,6 +99,10 @@ export interface IInviteState {
|
||||
events: IStrippedState[];
|
||||
}
|
||||
|
||||
export interface IKnockState {
|
||||
events: IStrippedState[];
|
||||
}
|
||||
|
||||
export interface IInvitedRoom {
|
||||
invite_state: IInviteState;
|
||||
}
|
||||
@ -109,10 +113,15 @@ export interface ILeftRoom {
|
||||
account_data: IAccountData;
|
||||
}
|
||||
|
||||
export interface IKnockedRoom {
|
||||
knock_state: IKnockState;
|
||||
}
|
||||
|
||||
export interface IRooms {
|
||||
[Category.Join]: Record<string, IJoinedRoom>;
|
||||
[Category.Invite]: Record<string, IInvitedRoom>;
|
||||
[Category.Leave]: Record<string, ILeftRoom>;
|
||||
[Category.Knock]: Record<string, IKnockedRoom>;
|
||||
}
|
||||
|
||||
interface IPresence {
|
||||
@ -156,6 +165,7 @@ export enum Category {
|
||||
Invite = "invite",
|
||||
Leave = "leave",
|
||||
Join = "join",
|
||||
Knock = "knock",
|
||||
}
|
||||
|
||||
interface IRoom {
|
||||
@ -196,6 +206,7 @@ function isTaggedEvent(event: IRoomEvent): event is TaggedEvent {
|
||||
export class SyncAccumulator {
|
||||
private accountData: Record<string, IMinimalEvent> = {}; // $event_type: Object
|
||||
private inviteRooms: Record<string, IInvitedRoom> = {}; // $roomId: { ... sync 'invite' json data ... }
|
||||
private knockRooms: Record<string, IKnockedRoom> = {}; // $roomId: { ... sync 'knock' json data ... }
|
||||
private joinRooms: { [roomId: string]: IRoom } = {};
|
||||
// the /sync token which corresponds to the last time rooms were
|
||||
// accumulated. We remember this so that any caller can obtain a
|
||||
@ -247,11 +258,17 @@ export class SyncAccumulator {
|
||||
this.accumulateRoom(roomId, Category.Leave, syncResponse.rooms.leave[roomId], fromDatabase);
|
||||
});
|
||||
}
|
||||
if (syncResponse.rooms.knock) {
|
||||
Object.keys(syncResponse.rooms.knock).forEach((roomId) => {
|
||||
this.accumulateRoom(roomId, Category.Knock, syncResponse.rooms.knock[roomId], fromDatabase);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private accumulateRoom(roomId: string, category: Category.Invite, data: IInvitedRoom, fromDatabase: boolean): void;
|
||||
private accumulateRoom(roomId: string, category: Category.Join, data: IJoinedRoom, fromDatabase: boolean): void;
|
||||
private accumulateRoom(roomId: string, category: Category.Leave, data: ILeftRoom, fromDatabase: boolean): void;
|
||||
private accumulateRoom(roomId: string, category: Category.Knock, data: IKnockedRoom, fromDatabase: boolean): void;
|
||||
private accumulateRoom(roomId: string, category: Category, data: any, fromDatabase = false): void {
|
||||
// Valid /sync state transitions
|
||||
// +--------+ <======+ 1: Accept an invite
|
||||
@ -269,6 +286,10 @@ export class SyncAccumulator {
|
||||
this.accumulateInviteState(roomId, data as IInvitedRoom);
|
||||
break;
|
||||
|
||||
case Category.Knock:
|
||||
this.accumulateKnockState(roomId, data as IKnockedRoom);
|
||||
break;
|
||||
|
||||
case Category.Join:
|
||||
if (this.inviteRooms[roomId]) {
|
||||
// (1)
|
||||
@ -326,6 +347,36 @@ export class SyncAccumulator {
|
||||
});
|
||||
}
|
||||
|
||||
private accumulateKnockState(roomId: string, data: IKnockedRoom): void {
|
||||
if (!data.knock_state || !data.knock_state.events) {
|
||||
// no new data
|
||||
return;
|
||||
}
|
||||
if (!this.knockRooms[roomId]) {
|
||||
this.knockRooms[roomId] = {
|
||||
knock_state: data.knock_state,
|
||||
};
|
||||
return;
|
||||
}
|
||||
// accumulate extra keys
|
||||
// clobber based on event type / state key
|
||||
// We expect knock_state to be small, so just loop over the events
|
||||
const currentData = this.knockRooms[roomId];
|
||||
data.knock_state.events.forEach((e) => {
|
||||
let hasAdded = false;
|
||||
for (let i = 0; i < currentData.knock_state.events.length; i++) {
|
||||
const current = currentData.knock_state.events[i];
|
||||
if (current.type === e.type && current.state_key == e.state_key) {
|
||||
currentData.knock_state.events[i] = e; // update
|
||||
hasAdded = true;
|
||||
}
|
||||
}
|
||||
if (!hasAdded) {
|
||||
currentData.knock_state.events.push(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Accumulate timeline and state events in a room.
|
||||
private accumulateJoinState(roomId: string, data: IJoinedRoom, fromDatabase = false): void {
|
||||
// We expect this function to be called a lot (every /sync) so we want
|
||||
@ -485,6 +536,7 @@ export class SyncAccumulator {
|
||||
const data: IRooms = {
|
||||
join: {},
|
||||
invite: {},
|
||||
knock: {},
|
||||
// always empty. This is set by /sync when a room was previously
|
||||
// in 'invite' or 'join'. On fresh startup, the client won't know
|
||||
// about any previous room being in 'invite' or 'join' so we can
|
||||
@ -501,6 +553,9 @@ export class SyncAccumulator {
|
||||
Object.keys(this.inviteRooms).forEach((roomId) => {
|
||||
data.invite[roomId] = this.inviteRooms[roomId];
|
||||
});
|
||||
Object.keys(this.knockRooms).forEach((roomId) => {
|
||||
data.knock[roomId] = this.knockRooms[roomId];
|
||||
});
|
||||
Object.keys(this.joinRooms).forEach((roomId) => {
|
||||
const roomData = this.joinRooms[roomId];
|
||||
const roomJson: IJoinedRoom = {
|
||||
|
27
src/sync.ts
27
src/sync.ts
@ -40,6 +40,7 @@ import {
|
||||
IInviteState,
|
||||
IJoinedRoom,
|
||||
ILeftRoom,
|
||||
IKnockedRoom,
|
||||
IMinimalEvent,
|
||||
IRoomEvent,
|
||||
IStateEvent,
|
||||
@ -1240,6 +1241,7 @@ export class SyncApi {
|
||||
let inviteRooms: WrappedRoom<IInvitedRoom>[] = [];
|
||||
let joinRooms: WrappedRoom<IJoinedRoom>[] = [];
|
||||
let leaveRooms: WrappedRoom<ILeftRoom>[] = [];
|
||||
let knockRooms: WrappedRoom<IKnockedRoom>[] = [];
|
||||
|
||||
if (data.rooms) {
|
||||
if (data.rooms.invite) {
|
||||
@ -1251,6 +1253,9 @@ export class SyncApi {
|
||||
if (data.rooms.leave) {
|
||||
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
|
||||
}
|
||||
if (data.rooms.knock) {
|
||||
knockRooms = this.mapSyncResponseToRoomArray(data.rooms.knock);
|
||||
}
|
||||
}
|
||||
|
||||
this.notifEvents = [];
|
||||
@ -1511,6 +1516,26 @@ export class SyncApi {
|
||||
});
|
||||
});
|
||||
|
||||
// Handle knocks
|
||||
await promiseMapSeries(knockRooms, async (knockObj) => {
|
||||
const room = knockObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
|
||||
|
||||
await this.injectRoomEvents(room, stateEvents);
|
||||
|
||||
if (knockObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for knock->leave->knock cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
});
|
||||
|
||||
// update the notification timeline, if appropriate.
|
||||
// we only do this for live events, as otherwise we can't order them sanely
|
||||
// in the timeline relative to ones paginated in by /notifications.
|
||||
@ -1629,7 +1654,7 @@ export class SyncApi {
|
||||
);
|
||||
}
|
||||
|
||||
private mapSyncResponseToRoomArray<T extends ILeftRoom | IJoinedRoom | IInvitedRoom>(
|
||||
private mapSyncResponseToRoomArray<T extends ILeftRoom | IJoinedRoom | IInvitedRoom | IKnockedRoom>(
|
||||
obj: Record<string, T>,
|
||||
): Array<WrappedRoom<T>> {
|
||||
// Maps { roomid: {stuff}, roomid: {stuff} }
|
||||
|
Reference in New Issue
Block a user