diff --git a/proposals/2753-peeking-via-sync-v2.md b/proposals/2753-peeking-via-sync-v2.md new file mode 100644 index 00000000000..763cf88a44d --- /dev/null +++ b/proposals/2753-peeking-via-sync-v2.md @@ -0,0 +1,301 @@ +# MSC2753: Peeking via Sync (Take 2) + +## Problem + +[Room previews](https://matrix.org/docs/spec/client_server/r0.6.1#id116), more +commonly known as "peeking", has a number of usecases, such as: + + * Look at a room before joining it, to preview it. + * Look at a user's profile room (see + [MSC1769](https://github.com/matrix-org/matrix-doc/issues/1769)). + * Browse the metadata or membership of a space (see + [MSC1772](https://github.com/matrix-org/matrix-doc/issues/1772)). + * Monitor [moderation policy lists](https://matrix.org/docs/spec/client_server/r0.6.1#moderation-policy-lists). + +Currently, peeking relies on the deprecated v1 `/initialSync` and `/events` +APIs. + +This poses the following issues: + + * Servers and clients must implement two separate sets of event-syncing logic, + doubling complexity. + * Peeking a room involves setting a stream of long-lived `/events` requests + going. Having multiple streams is racey, competes for resources with the + `/sync` stream, and doesn't scale given each room requires a new `/events` + stream. + * `/initialSync` and `/events` are deprecated and not implemented on new + servers. + +This proposal suggests a new API in which events in peeked rooms would be +returned over `/sync`. + +## Proposal + +Peeking into a room remains per-device: if the user has multiple devices, each +of which wants to peek into a given room, then each device must make a separate +request. + +To help avoid situations where clients create peek requests and forget about +them, each peek request is given a lifetime by the server. The client must +*renew* the peek request before this lifetime expires. The server is free to +pick any lifetime. + +### Starting a peek + +We add an CS API called `/peek`, which starts peeking into a given +room. This is similar to +[`/join/{roomIdOrAlias}`](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-join-roomidoralias) +but has a slightly different API shape. + +For example: + +``` +POST /_matrix/client/r0/peek/{roomIdOrAlias} HTTP/1.1 + +{ + "servers": [ + "server1", "server2" + ] +} +``` + +A successful response has the following format: + +``` +{ + "room_id": "", + "peek_expiry_ts": 1605534210000 +} +``` + +The `servers` parameter is optional and, if present, gives a list of servers to +try to peek through. + +XXX: should we limit this API to room IDs, and require clients to do a `GET +/_matrix/client/r0/directory/room/{roomAlias}` request if they have a room +alias? (In which case, `/_matrix/client/r0/room/{room_id}/peek` might be a +better name for it.) On the one hand: cleaner, simpler API. On the other: more +requests needed for each operation. + +Both `room_id` and `peek_expiry_ts` are required in the +response. `peek_expiry_ts` gives a timestamp (milliseconds since the unix +epoch) when the server will *expire* the peek if the client does not renew it. + +The server ratelimit requests to `/peek` and returns a 429 error with +`M_LIMIT_EXCEEDED` if the limit is exceeded. + +Otherwise, the server first resolves the given room alias to a room ID, if +needed. + +If there is already an active peek for the room in question, it is renewed and +a successful response is returned with the updated `peek_expiry_ts`. + +If the user is already *joined* to the room in question, the server returns a +400 error with `M_BAD_STATE`. + +If the room in question does not exist, the server returns a 404 error with +`M_NOT_FOUND`. + +If the room does not allow peeking (ie, it does not have `history_visibility` +of `world_readable` [1](#f1)), the server returns a 403 +error with `M_FORBIDDEN`. + +Otherwise, the server starts a peek for the calling device into the given room, +and returns a 200 response as above. + +When a peek first starts, the current state of the room is returned in the +`peek` section of the next `/sync` response. + +### Stopping a peek + +To stop peeking, the client calls `rooms//unpeek`: + +``` +POST /_matrix/client/r0/rooms/{room_id}/unpeek HTTP/1.1 + +{} +``` + +The body must be a JSON dictionary, but no parameters are specified. + +A successful response has an empty body. + +If the room is unknown or was not previously being peeked the server returns a +400 error with `M_BAD_STATE`. + +### `/sync` response + +We add a new `peek` section to the `rooms` field of the `/sync` +response. `peek` has the same shape as `join`. + +While a peek into a given room is active, any new events in the room cause that +room to be included in the `peek` section (but only for the device with the +active peek). When a peek first starts, the entire state of the room is +included in the same way as when a room is first joined. + +If the client requests lazy-loading via `lazy_load_members`, then redundant +membership events are excluded in the same way as they are for joined rooms. + +If a user subsequently `/join`s a room they are peeking, the room will +thenceforth appear in the `join` section instead of `peek`. For devices which +were already peeking into the room, the server should *not* include the entire +room state for the room in the `/sync` response, allowing the client to build +on the state and history it has already received without re-sending it down +`/sync`. + +When a room stops being peeked (either because the client called `/unpeek` or +because the server timed out the peek), the room will be included in the +`leave` section of the `/sync` response, including any events that occured +between the previous `/sync` and the the peek ending. If there are no such +events, the room's entry in the `leave` section will be empty. + +For example: + +```js +{ + "rooms": { + "join": { /* ... */ }, + "leave": { + "!unpeeked:example.org": { + "timeline": { + "events": [ + { "type": "m.room.message", "content": {"body": "just one more thing"}} + ] + } + }, + "!alsounpeeked:example.com": {} + } + } +} +``` + +## Encrypted rooms + +(this para taken from MSC #2444): + +It is considered a feature that you cannot peek into encrypted rooms, given +the act of peeking would leak the identity of the peeker to the joined users +in the room (as they'd need to encrypt for the peeker). This also feels +acceptable given there is little point in encrypting something intended to be +world-readable. + +## Future extensions + + * "snapshot" API, for a one-time peek operation which returns the current + state of the room without adding the room to future `/sync` responses. Might + be useful for certain usecases (eg, looking at a user's public profile)? + + * "bulk peek" API, for peeking into many rooms at once. Might be useful for + flair (which requires peeking into lots of users' profile rooms), though + realistically that usecase will need server-side support. + + * "cross-device" peeks could be useful for microblogging etc? + +## Potential issues + + * Expiring peeks might be hard for clients to manage? + +## Alternatives + +### Keep /peek closer to /join + +Given that peeking has parallels to joining, it might be preferable to keep the +API closer to `/join`. On the other hand, they probabably aren't actually +similar enough to make it worth propagating the oddities of `/join` (in +particular, the use of query-parameters +([matrix-doc#2864](https://github.com/matrix-org/matrix-doc/issues/2864)). + +### Filter-based API + +[MSC1776](https://github.com/matrix-org/matrix-doc/pull/1776) defined an +alternative approach, where you could use filters to add peeked rooms into a +given `/sync` response as needed. This however had some issues: + + * You can't specify what servers to peek remote rooms via. + * You can't identify rooms via alias, only ID + * It feels hacky to bodge peeked rooms into the `join` block of a given + `/sync` response + * Fiddling around with custom filters feels clunky relative to just calling a + `/peek` endpoint similar to `/join`. + +### Use the `join` block + +It could be seen as controversial to add another new block to the `/sync` +response. We could use the existing `join` block, but: + + * it's a misnomer (given the user hasn't joined the rooms) + * `join` refers to rooms which the *user* is in, rather than that they are + peeking into using a given *device* + * we risk breaking clients who aren't aware of the new style of peeking. + * there's already a precedent for per-device blocks in the sync response (for + to-device messages) + +### Per-account peeks + +It could be seen as controversial to make peeking a per-device rather than +per-user feature. When thinking through use cases for peeking, however: + + 1. Peeking a chatroom before joining it is the common case, and is definitely + per-device - you would not expect peeked rooms to randomly pop up on other + devices, or consume their bandwidth. + 2. [MSC1769](https://github.com/matrix-org/matrix-doc/pull/1769) (Profiles as + rooms) is also per device: if a given client wants to look at the Member + Info for a given user in a room, it shouldn't pollute the others with that + data. + 3. [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) (Groups as + rooms) uses room joins to indicate your own membership, and peeks to query + the group membership of other users. Similarly to profiles, it's not clear + that this should be per-user rather than per-device (and worse case, it's a + matter of effectively opting in rather than trying to filter out peeks you + don't care about). + +### Alternatives to expiring peeks + +Having servers expire peek requests could be fiddly, so we considered a number +of alternatives: + + * Allow peeks to stack up without limit and trust that clients will not forget + about them: after all, it is in clients' best interest not to leak + resources, to reduce the amount of data to be handled, and it is not obvious + that leaking peeks is easier than leaking joins. + + Ultimately this does not align with our experience of administering + `matrix.org`: it seems that where a resource *can* be leaked, it ultimately + will be, and it is better to design the API to prevent it. + + * Limit the number of peeks that can be active at once, to force clients to be + fastidious in their peek cleanups. However, it is hard to see what a good + limit would be. Furthermore: peeks could be lost through no fault of the + client (for example: when a `/peek` request succeeds but the client + does not receive the response), and these leaked peaks could stack up until + peeking becomes inoperative. + + * Automatically clear active peeks when a `/sync` request is made without a + `since` parameter. However, this feels like magic at a distance, and also + means that if you initial-sync separately (e.g. you stole an access token + from the DB to manually debug something) then existing clients will be + broken. + + * Have the client resubmit the list of active peeks every time it wants to add + or remove one. This could amount to a sigificant quantity of data. + +## Security considerations + +Servers should ratelimit calls to `/peek` to stop someone DoSing the +server. + +## Unstable prefix + +The following mapping will be used for identifiers in this MSC during +development: + +Proposed final identifier | Purpose | Development identifier +------------------------------- | ------- | ---- +`/_matrix/client/r0/peek` | API endpoint | `/_matrix/client/unstable/org.matrix.msc2753/peek` +`/_matrix/client/r0/rooms/{roomId}/unpeek` | API endpoint | `/_matrix/client/unstable/org.matrix.msc2753/rooms/{roomId}/unpeek` + +## Footnotes + +[1]: `join_rules` do not affect peekability - it's +possible to have an invite-only room which joe public can still peek into, if +`history_visibility` has been set to `world_readable`.[↩](#a1)