Merge pull request #1502 from matrix-org/rav/room_upgrades
MSC 1501: Room version upgradespull/977/head
commit
42f7a21fc8
@ -0,0 +1,370 @@
|
|||||||
|
# Room version upgrades
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
[MSC1425](https://github.com/matrix-org/matrix-doc/issues/1425) introduces a
|
||||||
|
mechanism for associating a "version" with a room, which allows us to introduce
|
||||||
|
changes to the mechanics of rooms.
|
||||||
|
|
||||||
|
Assuming that a given change is successful, the next challenge is to introduce
|
||||||
|
it to existing rooms. This proposal introduces a mechanism for doing so.
|
||||||
|
|
||||||
|
## Proposal
|
||||||
|
|
||||||
|
In short: room upgrades are implemented by creating a new room, shutting down
|
||||||
|
the old one, and linking between the two.
|
||||||
|
|
||||||
|
The mechanics of this are as follows. When Alice upgrades a room, her client
|
||||||
|
hits a new C-S api:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /_matrix/client/r0/rooms/{roomId}/upgrade
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"new_version": "2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"replacement_room": "!QtykxKocfsgujksjgd:matrix.org"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When this is called, the server:
|
||||||
|
|
||||||
|
* Checks that Alice has permissions to send `m.room.tombstone` state events.
|
||||||
|
|
||||||
|
* Creates a replacement room, with an `m.room.create` with a `predecessor` field
|
||||||
|
which links to the last known event in the old room:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sender": "@alice:somewhere.com",
|
||||||
|
"type": "m.room.create",
|
||||||
|
"state_key": "",
|
||||||
|
"room_id": "!QtykxKocfsgujksjgd:matrix.org",
|
||||||
|
"content": {
|
||||||
|
"version": "2",
|
||||||
|
"predecessor": {
|
||||||
|
"room_id": "!cURbaf:matrix.org",
|
||||||
|
"event_id": "$1235135aksjgdkg:matrix.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Replicates PL/privacy/topic/etc events to the new room.
|
||||||
|
|
||||||
|
* Moves any local aliases to the new room.
|
||||||
|
|
||||||
|
* Sends an `m.room.tombstone` state event in the old room to tell participants
|
||||||
|
that it is dead:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sender": "@alice:somewhere.com",
|
||||||
|
"type": "m.room.tombstone",
|
||||||
|
"state_key": "",
|
||||||
|
"room_id": "!cURbaf:matrix.org",
|
||||||
|
"content": {
|
||||||
|
"body": "This room has been replaced",
|
||||||
|
"replacement_room": "!QtykxKocfsgujksjgd:matrix.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `body` of the tombstone event is defined by the server (for now, at
|
||||||
|
least).
|
||||||
|
|
||||||
|
* Assuming Alice has the powers to do so, sets the power levels in the old
|
||||||
|
room to stop people speaking. In practice, this means setting
|
||||||
|
`events_default` and `invite` to the greater of `50` and `users_default+1`.
|
||||||
|
|
||||||
|
Bob's client understands the `m.room.tombstone` event, and:
|
||||||
|
|
||||||
|
* Hides the old room in the room list (the room continues to be accessible
|
||||||
|
via the old room id (permalinks, backlinks from the new room, etc).
|
||||||
|
|
||||||
|
* Displays, at the very bottom of the timeline of the old room: "This room
|
||||||
|
has been upgraded. Click here to follow the conversation to the new room".
|
||||||
|
The link is simply a permalink to the new room. When Bob opens it, he will
|
||||||
|
get joined to the new room.
|
||||||
|
|
||||||
|
[Note that if Bob is on a version of synapse which doesn't understand room
|
||||||
|
versions, following the permalink will take him to a room view which churns
|
||||||
|
for a while and eventually fails. Synapse 0.33.3 should at least give a
|
||||||
|
sensible error code.]
|
||||||
|
|
||||||
|
If it turns out that the replacement room also has a tombstone event, the
|
||||||
|
client may automatically keep following the chain until it reaches a room
|
||||||
|
that isn't dead.
|
||||||
|
|
||||||
|
* Optional extension: if the user is in both rooms, then the "N unread
|
||||||
|
messages" banner when scrolled up in the old room could be made to track
|
||||||
|
messages in the new room (so in practice the user would only ever see the
|
||||||
|
hiatus between the versions if they scrolled all the way to the beginning
|
||||||
|
of the new room or the end of the old one.)
|
||||||
|
|
||||||
|
Bob's client also understands the `predecessor` field in the `m.room.create`, and:
|
||||||
|
|
||||||
|
* At the top of scrollback in the new room, displays: "This room is a
|
||||||
|
continuation of a previous room. Click here to see the old conversation."
|
||||||
|
The link is a permalink to the old room.
|
||||||
|
|
||||||
|
* Optional extensions might include things like extending room search to
|
||||||
|
work across two rooms.
|
||||||
|
|
||||||
|
### Client changes needed
|
||||||
|
|
||||||
|
* Ability for an op to view the current room version and upgrade it (by
|
||||||
|
hitting `/upgrade`).
|
||||||
|
|
||||||
|
* ~~Also the ability for an op to see what versions the servers in the
|
||||||
|
current room supports (nb via a cap API) and so how many users will get
|
||||||
|
locked out~~ (This is descoped for now.)
|
||||||
|
|
||||||
|
* Display `m.room.tombstone`s as a sticky message at the bottom of the old
|
||||||
|
room (perhaps replacing the message composer input) as “This room has been
|
||||||
|
replaced. Please click here to continue” or similar.
|
||||||
|
|
||||||
|
* When the user clicks the link, the client attempts to join the new room if
|
||||||
|
we are not already a member, and then switches to a view on the new room.
|
||||||
|
|
||||||
|
The client should supply the name of the server which sent the tombstone
|
||||||
|
event as the `server_name` for the `/join` request. (That being the most
|
||||||
|
likely server to have an up-to-date copy of the room - this is essentially
|
||||||
|
a workaround for [vector-im/riot-web#2925](https://github.com/vector-im/riot-web/issues/2925).)
|
||||||
|
|
||||||
|
* If the client sees a pair of rooms with a tombstone correctly joined to the
|
||||||
|
new room, it should hide the old one from the RoomList.
|
||||||
|
|
||||||
|
* If one backpaginates the new room to its creation, we should show the
|
||||||
|
`m.room.create` as “This room is a continuation of a previous room; click here
|
||||||
|
to view” (or similar).
|
||||||
|
|
||||||
|
* Search is extended to search old rooms (could be done clientside for now).
|
||||||
|
|
||||||
|
Future eye-candy:
|
||||||
|
|
||||||
|
* When one is viewing the old room:
|
||||||
|
|
||||||
|
* Rather than showing a sticky tombstone at the bottom, one should probably
|
||||||
|
have the “10 unread messages” section in the status bar which refers to
|
||||||
|
the current version of the room.
|
||||||
|
|
||||||
|
* We should probably also show the membership list of the current room. (Perhaps?)
|
||||||
|
|
||||||
|
## Future extensions
|
||||||
|
|
||||||
|
### Invite-only rooms
|
||||||
|
|
||||||
|
Invite-only rooms are not dealt with explicitly here; they are made tricky by
|
||||||
|
the fact that users in the old room won't be able to join the new room without
|
||||||
|
an invite.
|
||||||
|
|
||||||
|
For now, we are assuming that the main reasons for carrying out a room-version
|
||||||
|
upgrade (ie, security problems due to state resets and the like) do not apply
|
||||||
|
as strongly to invite-only rooms, and we have descoped them for now.
|
||||||
|
|
||||||
|
In future, we will most likely deal with them as follows:
|
||||||
|
|
||||||
|
* For local users, we need to first create an invite for each user in the
|
||||||
|
room. This is easy, if a bit high-overhead.
|
||||||
|
|
||||||
|
* For remote users:
|
||||||
|
|
||||||
|
* Alice's server could send invites; however this is likely to give an
|
||||||
|
unsatisfactory UX due to servers being offline, maybe not supporting the
|
||||||
|
new room version, and then spamming Bob with mysterious invites.
|
||||||
|
|
||||||
|
* We could change the auth rules to treat a membership of the old room as
|
||||||
|
equivalent to an invite to the new room. However, this is likely to
|
||||||
|
reintroduce the problems we are trying to solve by replacing the room in
|
||||||
|
the first place.
|
||||||
|
|
||||||
|
* We could create a new type of membership which acts like an invite for
|
||||||
|
the purposes of allowing users into invite-only rooms, but doesn't need
|
||||||
|
to be sent to remote servers.
|
||||||
|
|
||||||
|
### Parting users from the old room
|
||||||
|
|
||||||
|
It's not obvious if users should stay members of the old room indefinitely
|
||||||
|
(until they manually leave). Perhaps they should automatically leave after a
|
||||||
|
respectful period? What if the user leaves the *new* room?
|
||||||
|
|
||||||
|
For now, we'll assume that they stay members until they manually leave. We can
|
||||||
|
see how that feels in practice.
|
||||||
|
|
||||||
|
## Potential issues
|
||||||
|
|
||||||
|
* What about clients that don't understand tombstones?
|
||||||
|
|
||||||
|
* I think they'll just show you two separate rooms (both with the same
|
||||||
|
name), and you won't be able to talk in one of them. It's not great but it
|
||||||
|
will probably do.
|
||||||
|
|
||||||
|
* It's a shame that scrollback in the new room will show a load of joins
|
||||||
|
before you get to the link to the old room.
|
||||||
|
|
||||||
|
## Dismissed solutions
|
||||||
|
|
||||||
|
### Variations on this proposal
|
||||||
|
|
||||||
|
#### Have servers auto-join their users on upgrade
|
||||||
|
|
||||||
|
In order to make the upgrade more seamless, it might be good for servers to
|
||||||
|
automatically join any users that were in the old room to the new room.
|
||||||
|
|
||||||
|
In short, when Bob's server receives a tombstone event, it would attempt to
|
||||||
|
auto-join Bob to the new room, and updates any aliases it may have to point to
|
||||||
|
the new room.
|
||||||
|
|
||||||
|
It's worth noting that the join may not be successful: for example, because
|
||||||
|
Bob's server is too old, or because Bob has been blocked from joining the new
|
||||||
|
room, or because joins are generally flaky. In this case Bob might attempt a
|
||||||
|
rejoin via his client.
|
||||||
|
|
||||||
|
#### Have servers merge old and new rooms together
|
||||||
|
|
||||||
|
Instead of expecting clients to interpret tombstone events, servers could merge
|
||||||
|
/sync results together for the two rooms and present both as the old room in
|
||||||
|
/sync results - and then map from the old room ID back to the new one for API
|
||||||
|
calls.
|
||||||
|
|
||||||
|
(Note that doing this the other way around (mapping both rooms into the *new*
|
||||||
|
room ID) doesn't really work without massively confusing clients which have
|
||||||
|
cached data about the old room.)
|
||||||
|
|
||||||
|
This sounds pretty confusing though: would the server do this forever
|
||||||
|
for all users on the server?
|
||||||
|
|
||||||
|
#### have clients merge old and new rooms
|
||||||
|
|
||||||
|
At the top of scrollback for the new room, shows some sort of "messages from a
|
||||||
|
previous version of this room" divider, above which it shows messages from the
|
||||||
|
old room <sup name="a10">[1](#f10)</sup>.
|
||||||
|
|
||||||
|
This gets quite messy quite quickly. For instance, it will be confusing to see
|
||||||
|
any ongoing traffic in the old room (people whose servers didn't get the memo
|
||||||
|
about the tombstone; people leaving the old room; etc). Ultimately it's all
|
||||||
|
client polish though, so we can consider this for the future.
|
||||||
|
|
||||||
|
<b name="f10">1</b> It's probably worth noting that this alone is a somewhat
|
||||||
|
fundamental reworking of a bunch of complicated stuff in matrix-react-sdk (and
|
||||||
|
presumably likewise in the mobile clients). [↩](#a10)
|
||||||
|
|
||||||
|
### Counter-proposal: upgrade existing rooms in-place
|
||||||
|
|
||||||
|
The general idea here is to divide the room DAG into v1 and v2 parts. Servers
|
||||||
|
which do not support v2 will not see the v2 part of the DAG. This might look
|
||||||
|
like the below:
|
||||||
|
|
||||||
|
![dag](1501-split-dag.png)
|
||||||
|
|
||||||
|
In this model, room version is tracked as a state event:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "m.room.version",
|
||||||
|
"content": {
|
||||||
|
"version": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This event can only ever be sent by the **original room creator** - i.e., the
|
||||||
|
sender of the `m.room.create event` - even if the `m.room.power_levels` say
|
||||||
|
otherwise <sup name="a1">[1](#f1)</sup> <sup name="a2">[2](#f2)</sup>.
|
||||||
|
|
||||||
|
The DAG is now divided into parts where the state of m.room.version is 2, and
|
||||||
|
those where it is missing (and therefore implicitly 1).
|
||||||
|
|
||||||
|
When sending out federation transactions, v2 events are listed under a new
|
||||||
|
versioned_pdus key in the /send request:
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /_matrix/federation/v1/send/991079979
|
||||||
|
|
||||||
|
{
|
||||||
|
"origin_server_ts": 1404835423000,
|
||||||
|
"origin": "matrix.org",
|
||||||
|
"pdus": [],
|
||||||
|
"versioned_pdus": {
|
||||||
|
2: [...]
|
||||||
|
},
|
||||||
|
"edus": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Old servers which do not support v2 will therefore not receive any v2 parts of
|
||||||
|
the DAG, and transactions which only contain updates to the v2 part of the DAG
|
||||||
|
will be treated as no-ops. Servers should ignore entries under versioned_pdus
|
||||||
|
which correspond to versions they do not know about.
|
||||||
|
|
||||||
|
As a special case, `m.room_version events` themselves are treated as having the
|
||||||
|
version previously in force in the room, unless that was v1, in which case,
|
||||||
|
they are treated as being v2. This means that the upgrade from v1 to v2, *and*
|
||||||
|
the upgrade from v2 to v3, are included under `versioned_pdus[2]`, but the
|
||||||
|
upgrade from v3 to v4 is included under `versioned_pdus[3]`. If a server
|
||||||
|
receives an `m.room_version` event with a version it does not understand, it
|
||||||
|
must refuse to allow any other events into the upgraded part of the DAG (and
|
||||||
|
probably *should* refuse to allow any events into the DAG at all)
|
||||||
|
<sup name="a1">[3](#f3)</sup>. The reasons for this are as follows:
|
||||||
|
|
||||||
|
* We want to ensure that v1-only servers do not receive the room_version
|
||||||
|
event, since they don't know how to auth it correctly, and more importantly
|
||||||
|
we want to ensure that they can't get hold of it to construct bits of DAG
|
||||||
|
which refer to it, and would have to follow the v2 rules.
|
||||||
|
|
||||||
|
* However, putting subsequent upgrades in a place where older servers will see
|
||||||
|
it means that we can do more graceful upgrades in future by telling their
|
||||||
|
clients that the room seems to have been upgraded.
|
||||||
|
|
||||||
|
As normal, we require joining servers to declare their support for room
|
||||||
|
versions, and reject any which do not include the version of the room given by
|
||||||
|
the current room state.
|
||||||
|
|
||||||
|
In theory this is all that is required to ensure that v1-only servers never see
|
||||||
|
any v2 parts of the DAG (events are only sent via /send, and a v1 server should
|
||||||
|
have no way to reference v2 parts of the DAG); however we may want to consider
|
||||||
|
adding a "versions" parameter to API calls like /event and /state which allow a
|
||||||
|
server to specify an event_id to fill from, in case a (non-synapse, presumably)
|
||||||
|
implementation gets an event id from a /context request or similar and tries to
|
||||||
|
fill from it.
|
||||||
|
|
||||||
|
The experience for clients could be improved by awareness of the m.room.version
|
||||||
|
event and querying a capabilities API (e.g. /versions?) to determine whether a
|
||||||
|
room has been upgraded newer than the server can support. If so, a client
|
||||||
|
could say: “Your server must be upgraded to continue to participate in this
|
||||||
|
room” (and manually prevent the user from trying to speak).
|
||||||
|
|
||||||
|
#### Problems
|
||||||
|
|
||||||
|
* This leaves users on old v1-only servers in their own view of the room,
|
||||||
|
potentially continuing to speak but only seeing a subset of events (those
|
||||||
|
from their own and other v1 servers).
|
||||||
|
|
||||||
|
In the worst case, they might ask questions which those on v2 servers can see but have no way of replying to.
|
||||||
|
|
||||||
|
One way to mitigate this would be to ensure that the last event sent in the
|
||||||
|
v1 part of the DAG is an m.room.power_levels to make the room read-only to
|
||||||
|
casual users; then restore the situation in the v2 DAG.
|
||||||
|
|
||||||
|
* There's nothing to stop "inquisitive" users on v1 servers sending
|
||||||
|
m.room.version events, which is likely to completely split-brain the room.
|
||||||
|
|
||||||
|
* This doesn't help us do things like change the format of the room id.
|
||||||
|
|
||||||
|
<b name="f1">1</b> This avoids the version event itself being vulnerable to state
|
||||||
|
resets. [↩](#a1)
|
||||||
|
|
||||||
|
<b name="f2">2</b> This is therefore a change to the event authorisation rules
|
||||||
|
which we need to introduce with room version 2. [↩](#a2)
|
||||||
|
|
||||||
|
<b name="f3">3</b> It's worth noting this does give the room creator a
|
||||||
|
kill-switch for the room, even if they've subsequently been de-opped. Such is
|
||||||
|
life. [↩](#a3)
|
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Loading…
Reference in New Issue