Skip to content

Commit 62c93f5

Browse files
authored
Merge pull request #1818 from matrix-org/t3chguy/fix/18089
Add support for /hierarchy API and fall back to /spaces API if needed
2 parents 96e8f65 + 14b424e commit 62c93f5

File tree

3 files changed

+227
-3
lines changed

3 files changed

+227
-3
lines changed

src/@types/spaces.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,46 @@ limitations under the License.
1515
*/
1616

1717
import { IPublicRoomsChunkRoom } from "../client";
18+
import { RoomType } from "./event";
19+
import { IStrippedState } from "../sync-accumulator";
1820

1921
// Types relating to Rooms of type `m.space` and related APIs
2022

2123
/* eslint-disable camelcase */
24+
/** @deprecated Use hierarchy instead where possible. */
2225
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
2326
num_refs: number;
2427
room_type: string;
2528
}
2629

30+
/** @deprecated Use hierarchy instead where possible. */
2731
export interface ISpaceSummaryEvent {
2832
room_id: string;
2933
event_id: string;
3034
origin_server_ts: number;
3135
type: string;
3236
state_key: string;
37+
sender: string;
3338
content: {
3439
order?: string;
3540
suggested?: boolean;
3641
auto_join?: boolean;
3742
via?: string[];
3843
};
3944
}
45+
46+
export interface IHierarchyRelation extends IStrippedState {
47+
room_id: string;
48+
origin_server_ts: number;
49+
content: {
50+
order?: string;
51+
suggested?: boolean;
52+
via?: string[];
53+
};
54+
}
55+
56+
export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
57+
room_type?: RoomType | string;
58+
children_state: IHierarchyRelation[];
59+
}
4060
/* eslint-enable camelcase */

src/client.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ import {
140140
SearchOrderBy,
141141
} from "./@types/search";
142142
import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
143-
import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
143+
import { IHierarchyRoom, ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
144144
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
145145
import { IThreepid } from "./@types/threepids";
146146
import { CryptoStore } from "./crypto/store/base";
@@ -7975,14 +7975,15 @@ export class MatrixClient extends EventEmitter {
79757975
}
79767976

79777977
/**
7978-
* Fetches or paginates a summary of a space as defined by MSC2946
7978+
* Fetches or paginates a summary of a space as defined by an initial version of MSC2946
79797979
* @param {string} roomId The ID of the space-room to use as the root of the summary.
79807980
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
79817981
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
79827982
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
79837983
* @param {number?} limit The maximum number of rooms to return in total.
79847984
* @param {string?} batch The opaque token to paginate a previous summary request.
7985-
* @returns {Promise} the response, with next_batch, rooms, events fields.
7985+
* @returns {Promise} the response, with next_token, rooms fields.
7986+
* @deprecated in favour of `getRoomHierarchy` due to the MSC changing paths.
79867987
*/
79877988
public getSpaceSummary(
79887989
roomId: string,
@@ -8007,6 +8008,60 @@ export class MatrixClient extends EventEmitter {
80078008
});
80088009
}
80098010

8011+
/**
8012+
* Fetches or paginates a room hierarchy as defined by MSC2946.
8013+
* Falls back gracefully to sourcing its data from `getSpaceSummary` if this API is not yet supported by the server.
8014+
* @param {string} roomId The ID of the space-room to use as the root of the summary.
8015+
* @param {number?} limit The maximum number of rooms to return per page.
8016+
* @param {number?} maxDepth The maximum depth in the tree from the root room to return.
8017+
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
8018+
* @param {string?} fromToken The opaque token to paginate a previous request.
8019+
* @returns {Promise} the response, with next_batch & rooms fields.
8020+
*/
8021+
public getRoomHierarchy(
8022+
roomId: string,
8023+
limit?: number,
8024+
maxDepth?: number,
8025+
suggestedOnly = false,
8026+
fromToken?: string,
8027+
): Promise<{
8028+
rooms: IHierarchyRoom[];
8029+
next_batch?: string; // eslint-disable-line camelcase
8030+
}> {
8031+
const path = utils.encodeUri("/rooms/$roomId/hierarchy", {
8032+
$roomId: roomId,
8033+
});
8034+
8035+
return this.http.authedRequest(undefined, "GET", path, {
8036+
suggested_only: suggestedOnly,
8037+
max_depth: maxDepth,
8038+
from: fromToken,
8039+
limit,
8040+
}, undefined, {
8041+
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
8042+
}).catch(e => {
8043+
if (e.errcode === "M_UNRECOGNIZED") {
8044+
// fall back to the older space summary API as it exposes the same data just in a different shape.
8045+
return this.getSpaceSummary(roomId, undefined, suggestedOnly, undefined, limit)
8046+
.then(({ rooms, events }) => {
8047+
// Translate response from `/spaces` to that we expect in this API.
8048+
const roomMap = new Map(rooms.map(r => {
8049+
return [r.room_id, <IHierarchyRoom>{ ...r, children_state: [] }];
8050+
}));
8051+
events.forEach(e => {
8052+
roomMap.get(e.room_id)?.children_state.push(e);
8053+
});
8054+
8055+
return {
8056+
rooms: Array.from(roomMap.values()),
8057+
};
8058+
});
8059+
}
8060+
8061+
throw e;
8062+
});
8063+
}
8064+
80108065
/**
80118066
* Creates a new file tree space with the given name. The client will pick
80128067
* defaults for how it expects to be able to support the remaining API offered

src/room-hierarchy.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/**
18+
* @module room-hierarchy
19+
*/
20+
21+
import { Room } from "./models/room";
22+
import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces";
23+
import { MatrixClient } from "./client";
24+
import { EventType } from "./@types/event";
25+
26+
export class RoomHierarchy {
27+
// Map from room id to list of servers which are listed as a via somewhere in the loaded hierarchy
28+
public readonly viaMap = new Map<string, Set<string>>();
29+
// Map from room id to list of rooms which claim this room as their child
30+
public readonly backRefs = new Map<string, string[]>();
31+
// Map from room id to object
32+
public readonly roomMap = new Map<string, IHierarchyRoom>();
33+
private loadRequest: ReturnType<MatrixClient["getRoomHierarchy"]>;
34+
private nextBatch?: string;
35+
private _rooms?: IHierarchyRoom[];
36+
private serverSupportError?: Error;
37+
38+
/**
39+
* Construct a new RoomHierarchy
40+
*
41+
* A RoomHierarchy instance allows you to easily make use of the /hierarchy API and paginate it.
42+
*
43+
* @param {Room} root the root of this hierarchy
44+
* @param {number} pageSize the maximum number of rooms to return per page, can be overridden per load request.
45+
* @param {number} maxDepth the maximum depth to traverse the hierarchy to
46+
* @param {boolean} suggestedOnly whether to only return rooms with suggested=true.
47+
* @constructor
48+
*/
49+
constructor(
50+
private readonly root: Room,
51+
private readonly pageSize?: number,
52+
private readonly maxDepth?: number,
53+
private readonly suggestedOnly = false,
54+
) {}
55+
56+
public get noSupport(): boolean {
57+
return !!this.serverSupportError;
58+
}
59+
60+
public get canLoadMore(): boolean {
61+
return !!this.serverSupportError || !!this.nextBatch || !this._rooms;
62+
}
63+
64+
public get rooms(): IHierarchyRoom[] {
65+
return this._rooms;
66+
}
67+
68+
public async load(pageSize = this.pageSize): Promise<IHierarchyRoom[]> {
69+
if (this.loadRequest) return this.loadRequest.then(r => r.rooms);
70+
71+
this.loadRequest = this.root.client.getRoomHierarchy(
72+
this.root.roomId,
73+
pageSize,
74+
this.maxDepth,
75+
this.suggestedOnly,
76+
this.nextBatch,
77+
);
78+
79+
let rooms: IHierarchyRoom[];
80+
try {
81+
({ rooms, next_batch: this.nextBatch } = await this.loadRequest);
82+
} catch (e) {
83+
if (e.errcode === "M_UNRECOGNIZED") {
84+
this.serverSupportError = e;
85+
} else {
86+
throw e;
87+
}
88+
89+
return [];
90+
} finally {
91+
this.loadRequest = null;
92+
}
93+
94+
if (this._rooms) {
95+
this._rooms = this._rooms.concat(rooms);
96+
} else {
97+
this._rooms = rooms;
98+
}
99+
100+
rooms.forEach(room => {
101+
this.roomMap.set(room.room_id, room);
102+
103+
room.children_state.forEach(ev => {
104+
if (ev.type !== EventType.SpaceChild) return;
105+
const childRoomId = ev.state_key;
106+
107+
// track backrefs for quicker hierarchy navigation
108+
if (!this.backRefs.has(childRoomId)) {
109+
this.backRefs.set(childRoomId, []);
110+
}
111+
this.backRefs.get(childRoomId).push(ev.room_id);
112+
113+
// fill viaMap
114+
if (Array.isArray(ev.content.via)) {
115+
if (!this.viaMap.has(childRoomId)) {
116+
this.viaMap.set(childRoomId, new Set());
117+
}
118+
const vias = this.viaMap.get(childRoomId);
119+
ev.content.via.forEach(via => vias.add(via));
120+
}
121+
});
122+
});
123+
124+
return rooms;
125+
}
126+
127+
public getRelation(parentId: string, childId: string): IHierarchyRelation {
128+
return this.roomMap.get(parentId)?.children_state.find(e => e.state_key === childId);
129+
}
130+
131+
public isSuggested(parentId: string, childId: string): boolean {
132+
return this.getRelation(parentId, childId)?.content.suggested;
133+
}
134+
135+
// locally remove a relation as a form of local echo
136+
public removeRelation(parentId: string, childId: string): void {
137+
const backRefs = this.backRefs.get(childId);
138+
if (backRefs?.length === 1) {
139+
this.backRefs.delete(childId);
140+
} else if (backRefs?.length) {
141+
this.backRefs.set(childId, backRefs.filter(ref => ref !== parentId));
142+
}
143+
144+
const room = this.roomMap.get(parentId);
145+
if (room) {
146+
room.children_state = room.children_state.filter(ev => ev.state_key !== childId);
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)