|
| 1 | +# MSC2946: Spaces Summary |
| 2 | + |
| 3 | +This MSC depends on [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772), which |
| 4 | +describes why a Space is useful: |
| 5 | + |
| 6 | +> Collecting rooms together into groups is useful for a number of purposes. Examples include: |
| 7 | +> |
| 8 | +> * Allowing users to discover different rooms related to a particular topic: for example "official matrix.org rooms". |
| 9 | +> * Allowing administrators to manage permissions across a number of rooms: for example "a new employee has joined my company and needs access to all of our rooms". |
| 10 | +> * Letting users classify their rooms: for example, separating "work" from "personal" rooms. |
| 11 | +> |
| 12 | +> We refer to such collections of rooms as "spaces". |
| 13 | +
|
| 14 | +This MSC attempts to solve how a member of a space discovers rooms in that space. This |
| 15 | +is useful for quickly exposing a user to many aspects of an entire community, using the |
| 16 | +examples above, joining the "official matrix.org rooms" space might suggest joining a few |
| 17 | +rooms: |
| 18 | + |
| 19 | +* A room to discuss development of the Matrix Spec. |
| 20 | +* An announcements room for news related to matrix.org. |
| 21 | +* An off-topic room for members of the space. |
| 22 | + |
| 23 | +## Proposal |
| 24 | + |
| 25 | +A new client-server API (and corresponding server-server API) is added which allows |
| 26 | +for querying for the rooms and spaces contained within a space. This allows a client |
| 27 | +to efficiently display a hierarchy of rooms to a user (i.e. without having |
| 28 | +to walk the full state of each room). |
| 29 | + |
| 30 | +### Client-server API |
| 31 | + |
| 32 | +An endpoint is provided to walk the space tree, starting at the provided room ID |
| 33 | +("the root room"), and visiting other rooms/spaces found via `m.space.child` |
| 34 | +events. It recurses into the children and into their children, etc. |
| 35 | + |
| 36 | +Any child room that the user is joined or is potentially joinable (per |
| 37 | +[MSC3173](https://github.com/matrix-org/matrix-doc/pull/3173)) is included in |
| 38 | +the response. When a room with a `type` of `m.space` is found, it is searched |
| 39 | +for valid `m.space.child` events to recurse into. |
| 40 | + |
| 41 | +In order to provide a consistent experience, the space tree should be walked in |
| 42 | +a depth-first manner, e.g. whenever a space is found it should be recursed into |
| 43 | +by sorting the children rooms and iterating through them. |
| 44 | + |
| 45 | +There could be loops in the returned child events; clients and servers should |
| 46 | +handle this gracefully. Similarly, note that a child room might appear multiple |
| 47 | +times (e.g. also be a grandchild). Clients and servers should handle this |
| 48 | +appropriately. |
| 49 | + |
| 50 | +This endpoint requires authentication and is subject to rate-limiting. |
| 51 | + |
| 52 | +#### Request format |
| 53 | + |
| 54 | +```text |
| 55 | +GET /_matrix/client/v1/rooms/{roomID}/hierarchy |
| 56 | +``` |
| 57 | + |
| 58 | +Query Parameters: |
| 59 | + |
| 60 | +* **`suggested_only`**: Optional. If `true`, return only child events and rooms |
| 61 | + where the `m.space.child` event has `suggested: true`. Must be a boolean, |
| 62 | + defaults to `false`. |
| 63 | + |
| 64 | + This applies transitively, i.e. if a `suggested_only` is `true` and a space is |
| 65 | + not suggested then it should not be searched for children. The inverse is also |
| 66 | + true, if a space is suggested, but a child of that space is not then the child |
| 67 | + should not be included. |
| 68 | +* **`limit`**: Optional: a client-defined limit to the maximum |
| 69 | + number of rooms to return per page. Must an integer greater than zero. |
| 70 | + |
| 71 | + Server implementations should impose a maximum value to avoid resource |
| 72 | + exhaustion. |
| 73 | +* **`max_depth`**: Optional: The maximum depth in the tree (from the root room) |
| 74 | + to return. The deepest depth returned will not include children events. Defaults |
| 75 | + to no-limit. Must be a non-negative integer. |
| 76 | + |
| 77 | + Server implementations may wish to impose a maximum value to avoid resource |
| 78 | + exhaustion. |
| 79 | +* **`from`**: Optional. Pagination token given to retrieve the next set of rooms. |
| 80 | + |
| 81 | + Note that if a pagination token is provided, then the parameters given for |
| 82 | + `suggested_only` and `max_depth` must be the same. |
| 83 | + |
| 84 | +#### Response Format |
| 85 | + |
| 86 | +* **`rooms`**: `[object]` For each room/space, starting with the root room, a |
| 87 | + summary of that room. The fields are the same as those returned by |
| 88 | + `/publicRooms` (see |
| 89 | + [spec](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-publicrooms)), |
| 90 | + with the addition of: |
| 91 | + * **`room_type`**: the value of the `m.type` field from the room's |
| 92 | + `m.room.create` event, if any. |
| 93 | + * **`children_state`**: The stripped state of the `m.space.child` events of |
| 94 | + the room per [MSC3173](https://github.com/matrix-org/matrix-doc/pull/3173). |
| 95 | + In addition to the standard stripped state fields, the following is included: |
| 96 | + * **`origin_server_ts`**: `integer`. The `origin_server_ts` field from the |
| 97 | + room's `m.space.child` event. This is required for sorting of rooms as |
| 98 | + specified below. |
| 99 | +* **`next_batch`**: Optional `string`. The token to supply in the `from` param |
| 100 | + of the next `/hierarchy` request in order to request more rooms. If this is absent, |
| 101 | + there are no more results. |
| 102 | + |
| 103 | +#### Example request: |
| 104 | + |
| 105 | +```text |
| 106 | +GET /_matrix/client/v1/rooms/%21ol19s%3Ableecker.street/hierarchy? |
| 107 | + limit=30& |
| 108 | + suggested_only=true& |
| 109 | + max_depth=4 |
| 110 | +``` |
| 111 | + |
| 112 | +#### Example response: |
| 113 | + |
| 114 | +```jsonc |
| 115 | +{ |
| 116 | + "rooms": [ |
| 117 | + { |
| 118 | + "room_id": "!ol19s:bleecker.street", |
| 119 | + "avatar_url": "mxc://bleecker.street/CHEDDARandBRIE", |
| 120 | + "guest_can_join": false, |
| 121 | + "name": "CHEESE", |
| 122 | + "num_joined_members": 37, |
| 123 | + "topic": "Tasty tasty cheese", |
| 124 | + "world_readable": true, |
| 125 | + "join_rules": "public", |
| 126 | + "room_type": "m.space", |
| 127 | + "children_state": [ |
| 128 | + { |
| 129 | + "type": "m.space.child", |
| 130 | + "state_key": "!efgh:example.com", |
| 131 | + "content": { |
| 132 | + "via": ["example.com"], |
| 133 | + "suggested": true |
| 134 | + }, |
| 135 | + "room_id": "!ol19s:bleecker.street", |
| 136 | + "sender": "@alice:bleecker.street", |
| 137 | + "origin_server_ts": 1432735824653 |
| 138 | + }, |
| 139 | + { ... } |
| 140 | + ] |
| 141 | + }, |
| 142 | + { ... } |
| 143 | + ], |
| 144 | + "next_batch": "abcdef" |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +#### Errors: |
| 149 | + |
| 150 | +An HTTP response with a status code of 403 and an error code of `M_FORBIDDEN` |
| 151 | +should be returned if the user doesn't have permission to view/peek the root room. |
| 152 | +This should also be returned if that room does not exist, which matches the |
| 153 | +behavior of other room endpoints (e.g. |
| 154 | +[`/_matrix/client/r0/rooms/{roomID}/aliases`](https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-rooms-roomid-aliases)) |
| 155 | +to not divulge that a room exists which the user doesn't have permission to view. |
| 156 | + |
| 157 | +An HTTP response with a status code of 400 and an error code of `M_INVALID_PARAM` |
| 158 | +should be returned if the `from` token provided is unknown to the server or if |
| 159 | +the `suggested_only` or `max_depth` parameters are modified during pagination. |
| 160 | + |
| 161 | +#### Server behaviour |
| 162 | + |
| 163 | +The server should generate the response as discussed above, by doing a depth-first |
| 164 | +search (starting at the "root" room) for any `m.space.child` events. Any |
| 165 | +`m.space.child` with an invalid `via` are discarded (invalid is defined as in |
| 166 | +[MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772): missing, not an |
| 167 | +array or an empty array). |
| 168 | + |
| 169 | +In the case of the homeserver not having access to the state of a room, the |
| 170 | +server-server API (see below) can be used to query for this information over |
| 171 | +federation from one of the servers provided in the `via` key of the |
| 172 | +`m.space.child` event. It is recommended to cache the federation response for a |
| 173 | +period of time. The federation results may contain information on a room |
| 174 | +that the requesting server is already participating in; the requesting server |
| 175 | +should use its local data for such rooms rather than the data returned over |
| 176 | +federation. |
| 177 | + |
| 178 | +When the current response page is full, the current state should be persisted |
| 179 | +and a pagination token should be generated (if there is more data to return). |
| 180 | +To prevent resource exhaustion, the server may expire persisted data that it |
| 181 | +deems to be stale. |
| 182 | + |
| 183 | +The persisted state will include: |
| 184 | + |
| 185 | +* The processed rooms. |
| 186 | +* Rooms to process (in depth-first order with rooms at the same depth |
| 187 | + ordered [according to MSC1772, as updated to below](#msc1772-ordering)). |
| 188 | +* Room information from federation responses for rooms which have yet to be |
| 189 | + processed. |
| 190 | + |
| 191 | +### Server-server API |
| 192 | + |
| 193 | +The Server-Server API has a similar interface to the Client-Server API, but a |
| 194 | +simplified response. It is used when a homeserver is not participating in a room |
| 195 | +(and cannot summarize room due to not having the state). |
| 196 | + |
| 197 | +The main difference is that it does *not* recurse into spaces and does not support |
| 198 | +pagination. This is somewhat equivalent to a Client-Server request with a `max_depth=1`. |
| 199 | + |
| 200 | +Additional federation requests are made to recurse into sub-spaces. This allows |
| 201 | +for trivially caching responses for a short period of time (since it is not |
| 202 | +easily known the room summary might have changed). |
| 203 | + |
| 204 | +Since the server-server API does not know the requesting user, the response should |
| 205 | +divulge information based on if any member of the requesting server could join |
| 206 | +the room. The requesting server is trusted to properly filter this information |
| 207 | +using the `world_readable`, `join_rules`, and `allowed_room_ids` fields from the |
| 208 | +response. |
| 209 | + |
| 210 | +If the target server is not a member of some children rooms (so would have to send |
| 211 | +another request over federation to inspect them), no attempt is made to recurse |
| 212 | +into them. They are simply omitted from the `children` key of the response. |
| 213 | +(Although they will still appear in the `children_state`key of the `room`.) |
| 214 | + |
| 215 | +Similarly, if a server-set limit on the size of the response is reached, additional |
| 216 | +rooms are not added to the response and can be queried individually. |
| 217 | + |
| 218 | +#### Request format |
| 219 | + |
| 220 | +```text |
| 221 | +GET /_matrix/federation/v1/hierarchy/{roomID} |
| 222 | +``` |
| 223 | + |
| 224 | +Query Parameters: |
| 225 | + |
| 226 | +* **`suggested_only`**: The same as the Client-Server API. |
| 227 | + |
| 228 | +#### Response format |
| 229 | + |
| 230 | +The response format is similar to the Client-Server API: |
| 231 | + |
| 232 | +* **`room`**: `object` The summary of the requested room, see below for details. |
| 233 | +* **`children`**: `[object]` For each room/space, a summary of that room, see |
| 234 | + below for details. |
| 235 | +* **`inaccessible_children`**: Optional `[string]`. A list of room IDs which are |
| 236 | + children of the requested room, but are inaccessible to the requesting server. |
| 237 | + Assuming the target server is non-malicious and well-behaved, then other |
| 238 | + non-malicious servers should respond with the same set of inaccessible rooms. |
| 239 | + Thus the requesting server can consider the rooms inaccessible from everywhere. |
| 240 | + |
| 241 | + This is used to differentiate between rooms which the requesting server does |
| 242 | + not have access to from those that the target server cannot include in the |
| 243 | + response (which will simply be missing in the response). |
| 244 | + |
| 245 | +For both the `room` and `children` fields the summary of the room/space includes |
| 246 | +the fields returned by `/publicRooms` (see [spec](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-publicrooms)), |
| 247 | +with the addition of: |
| 248 | + |
| 249 | +* **`room_type`**: the value of the `m.type` field from the room's `m.room.create` |
| 250 | + event, if any. |
| 251 | +* **`allowed_room_ids`**: A list of room IDs which give access to this room per |
| 252 | + [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083).<sup id="a1">[1](#f1)</sup> |
| 253 | + |
| 254 | +#### Example request: |
| 255 | + |
| 256 | +```jsonc |
| 257 | +GET /_matrix/federation/v1/hierarchy/{roomID}? |
| 258 | + suggested_only=true |
| 259 | +``` |
| 260 | +
|
| 261 | +#### Errors: |
| 262 | +
|
| 263 | +An HTTP response with a status code of 404 and an error code of `M_NOT_FOUND` is |
| 264 | +returned if the target server is not a member of the requested room or the |
| 265 | +requesting server is not allowed to access the room. |
| 266 | +
|
| 267 | +### MSC1772 Ordering |
| 268 | +
|
| 269 | +[MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) defines the ordering |
| 270 | +of "default ordering of siblings in the room list" using the `order` key: |
| 271 | +
|
| 272 | +> Rooms are sorted based on a lexicographic ordering of the Unicode codepoints |
| 273 | +> of the characters in `order` values. Rooms with no `order` come last, in |
| 274 | +> ascending numeric order of the `origin_server_ts` of their `m.room.create` |
| 275 | +> events, or ascending lexicographic order of their `room_id`s in case of equal |
| 276 | +> `origin_server_ts`. `order`s which are not strings, or do not consist solely |
| 277 | +> of ascii characters in the range `\x20` (space) to `\x7F` (~), or consist of |
| 278 | +> more than 50 characters, are forbidden and the field should be ignored if |
| 279 | +> received. |
| 280 | +
|
| 281 | +Unfortunately there are situations when a homeserver comes across a reference to |
| 282 | +a child room that is unknown to it and must decide the ordering. Without being |
| 283 | +able to see the `m.room.create` event (which it might not have permission to see) |
| 284 | +no proper ordering can be given. |
| 285 | +
|
| 286 | +Consider the following case of a space with 3 child rooms: |
| 287 | +
|
| 288 | +``` |
| 289 | + Space A |
| 290 | + | |
| 291 | + +--------+--------+ |
| 292 | + | | | |
| 293 | +Room B Room C Room D |
| 294 | +``` |
| 295 | +
|
| 296 | +HS1 has users in Space A, Room B, and Room C, while HS2 has users in Room D. HS1 has no users |
| 297 | +in Room D (and thus has no state from it). Room B, C, and D do not have an |
| 298 | +`order` field set (and default to using the ordering rules above). |
| 299 | +
|
| 300 | +When a user asks HS1 for the space summary with a `limit` equal to `2` it cannot |
| 301 | +fulfill this request since it is unsure how to order Room B, Room C, and Room D, |
| 302 | +but it can only return 2 of them. It *can* reach out over federation to HS2 and |
| 303 | +request a space summary for Room D, but this is undesirable: |
| 304 | +
|
| 305 | +* HS1 might not have the permissions to know any of the state of Room D, so might |
| 306 | + receive a 404 error. |
| 307 | +* If we expand the example above to many rooms than this becomes expensive to |
| 308 | + query a remote server simply for ordering. |
| 309 | +
|
| 310 | +This proposes changing the ordering rules from MSC1772 to the following: |
| 311 | +
|
| 312 | +> Rooms are sorted based on a lexicographic ordering of the Unicode codepoints |
| 313 | +> of the characters in `order` values. Rooms with no `order` come last, in |
| 314 | +> ascending numeric order of the `origin_server_ts` of their `m.space.child` |
| 315 | +> events, or ascending lexicographic order of their `room_id`s in case of equal |
| 316 | +> `origin_server_ts`. `order`s which are not strings, or do not consist solely |
| 317 | +> of ascii characters in the range `\x20` (space) to `\x7E` (~), or consist of |
| 318 | +> more than 50 characters, are forbidden and the field should be ignored if |
| 319 | +> received. |
| 320 | +
|
| 321 | +This modifies the clause for calculating the order to use the `origin_server_ts` |
| 322 | +of the `m.space.child` event instead of the `m.room.create` event. This allows |
| 323 | +for a defined sorting of siblings based purely on the information available in |
| 324 | +the state of the space while still allowing for a natural ordering due to the |
| 325 | +age of the relationship. |
| 326 | +
|
| 327 | +## Potential issues |
| 328 | +
|
| 329 | +A large flat space (a single room with many `m.space.child` events) could cause |
| 330 | +a large federation response. |
| 331 | +
|
| 332 | +Room version upgrades of rooms in a space are unsolved and left to a future MSC. |
| 333 | +When upgrading a room it is unclear if the old room should be removed (in which |
| 334 | +case users who have not yet joined the new room will no longer see it in the space) |
| 335 | +or leave the old room (in which case users who have joined the new room will see |
| 336 | +both). The current recommendation is for clients de-duplicate rooms which are |
| 337 | +known old versions of rooms in the space. |
| 338 | +
|
| 339 | +## Alternatives |
| 340 | +
|
| 341 | +Peeking to explore the room state could be used to build the tree of rooms/spaces, |
| 342 | +but this would be significantly more expensive for both clients and servers. It |
| 343 | +would also require peeking over federation (which is explored in |
| 344 | +[MSC2444](https://github.com/matrix-org/matrix-doc/pull/2444)). |
| 345 | +
|
| 346 | +## Security considerations |
| 347 | +
|
| 348 | +A space with many sub-spaces and rooms on different homeservers could cause |
| 349 | +a large number of federation requests. A carefully crafted space with inadequate |
| 350 | +server enforced limits could be used in a denial of service attack. Generally |
| 351 | +this is mitigated by enforcing server limits and caching of responses. |
| 352 | +
|
| 353 | +The requesting server over federation is trusted to filter the response for the |
| 354 | +requesting user. The alternative, where the requesting server sends the requesting |
| 355 | +`user_id`, and the target server does the filtering, is unattractive because it |
| 356 | +rules out a caching of the result. This does not decrease security since a server |
| 357 | +could lie and make a request on behalf of a user in the proper space to see the |
| 358 | +given information. I.e. the calling server must be trusted anyway. |
| 359 | +
|
| 360 | +## Unstable prefix |
| 361 | +
|
| 362 | +During development of this feature it will be available at unstable endpoints. |
| 363 | +
|
| 364 | +The client-server API will be: |
| 365 | +`/_matrix/client/unstable/org.matrix.msc2946/rooms/{roomID}/hierarchy` |
| 366 | +
|
| 367 | +The server-server API will be: |
| 368 | +`/_matrix/federation/unstable/org.matrix.msc2946/hierarchy/{roomID}` |
| 369 | +
|
| 370 | +## Footnotes |
| 371 | +
|
| 372 | +<a id="f1"/>[1]: As a worked example, in the context of |
| 373 | +[MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083), consider that Alice |
| 374 | +and Bob share a server; Alice is a member of a space, but Bob is not. A remote |
| 375 | +server will not know whether the request is on behalf of Alice or Bob (and hence |
| 376 | +whether it should share details of restricted rooms within that space). |
| 377 | +
|
| 378 | +Consider if the space is modified to include a restricted room on a different server |
| 379 | +which allows access from the space. When summarizing the space, the homeserver must make |
| 380 | +a request over federation for information on the room. The response should include |
| 381 | +the room (since Alice is able to join it). Without additional information the |
| 382 | +calling server does not know *why* they received the room and cannot properly |
| 383 | +filter the returned results. |
| 384 | +
|
| 385 | +Note that there are still potential situations where each server individually |
| 386 | +doesn't have enough information to properly return the full summary, but these |
| 387 | +do not seem reasonable in what is considered a normal structure of spaces. (E.g. |
| 388 | +in the above example, if the remote server is not in the space and does not know |
| 389 | +whether the server is in the space or not it cannot return the room.)[↩](#a1) |
0 commit comments