Room previews, 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).
- Browse the metadata or membership of a space (see MSC1772).
- Monitor 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
.
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.
We add an CS API called /peek
, which starts peeking into a given
room. This is similar to
/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": "<resolved 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), 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.
To stop peeking, the client calls rooms/<id>/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
.
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:
{
"rooms": {
"join": { /* ... */ },
"leave": {
"!unpeeked:example.org": {
"timeline": {
"events": [
{ "type": "m.room.message", "content": {"body": "just one more thing"}}
]
}
},
"!alsounpeeked:example.com": {}
}
}
}
(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.
-
"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?
- Expiring peeks might be hard for clients to manage?
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).
MSC1776 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
.
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)
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:
- 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.
- MSC1769 (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.
- MSC1772 (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).
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 asince
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.
Servers should ratelimit calls to /peek
to stop someone DoSing the
server.
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 |
[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
.↩