This repository has been archived by the owner on Apr 26, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Room version upgrade support #4091
Merged
Merged
Changes from 10 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
871c4ab
Factor _generate_room_id out of create_room
richvdh 7f7b2cd
Make room_member_handler a member of RoomCreationHandler
richvdh e194817
Allow power_level_content_override=None for _send_events_for_new_room
richvdh 0f7d1c9
Basic initial support for room upgrades
richvdh 4cda300
preserve room visibility
richvdh 1b9f253
preserve PLs
richvdh 3a263bf
copy state
richvdh e6babc2
restrict PLs in old room
richvdh 68c0ce6
changelog
richvdh 474810d
fix broken test
richvdh 193cadc
Address review comments
richvdh 54bbe71
optimise state copying
richvdh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Support for replacing rooms with new ones |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,10 +32,11 @@ | |
JoinRules, | ||
RoomCreationPreset, | ||
) | ||
from synapse.api.errors import AuthError, Codes, StoreError, SynapseError | ||
from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError | ||
from synapse.storage.state import StateFilter | ||
from synapse.types import RoomAlias, RoomID, RoomStreamToken, StreamToken, UserID | ||
from synapse.util import stringutils | ||
from synapse.util.async_helpers import Linearizer | ||
from synapse.visibility import filter_events_for_client | ||
|
||
from ._base import BaseHandler | ||
|
@@ -73,6 +74,190 @@ def __init__(self, hs): | |
|
||
self.spam_checker = hs.get_spam_checker() | ||
self.event_creation_handler = hs.get_event_creation_handler() | ||
self.room_member_handler = hs.get_room_member_handler() | ||
|
||
# linearizer to stop two upgrades happening at once | ||
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer") | ||
|
||
@defer.inlineCallbacks | ||
def upgrade_room(self, requester, old_room_id, new_version): | ||
"""Replace a room with a new room with a different version | ||
|
||
Args: | ||
requester (synapse.types.Requester): the user requesting the upgrade | ||
old_room_id (unicode): the id of the room to be replaced | ||
new_version (unicode): the new room version to use | ||
|
||
Returns: | ||
Deferred[unicode]: the new room id | ||
""" | ||
yield self.ratelimit(requester) | ||
|
||
user_id = requester.user.to_string() | ||
|
||
with (yield self._upgrade_linearizer.queue(old_room_id)): | ||
# start by allocating a new room id | ||
r = yield self.store.get_room(old_room_id) | ||
if r is None: | ||
raise NotFoundError("Unknown room id %s" % (old_room_id,)) | ||
new_room_id = yield self._generate_room_id( | ||
creator_id=user_id, is_public=r["is_public"], | ||
) | ||
|
||
# we create and auth the tombstone event before properly creating the new | ||
# room, to check our user has perms in the old room. | ||
tombstone_event, tombstone_context = ( | ||
yield self.event_creation_handler.create_event( | ||
requester, { | ||
"type": EventTypes.Tombstone, | ||
"state_key": "", | ||
"room_id": old_room_id, | ||
"sender": user_id, | ||
"content": { | ||
"body": "This room has been replaced", | ||
"replacement_room": new_room_id, | ||
} | ||
}, | ||
token_id=requester.access_token_id, | ||
) | ||
) | ||
yield self.auth.check_from_context(tombstone_event, tombstone_context) | ||
|
||
yield self.clone_exiting_room( | ||
requester, | ||
old_room_id=old_room_id, | ||
new_room_id=new_room_id, | ||
new_room_version=new_version, | ||
tombstone_event_id=tombstone_event.event_id, | ||
) | ||
|
||
# now send the tombstone | ||
yield self.event_creation_handler.send_nonmember_event( | ||
requester, tombstone_event, tombstone_context, | ||
) | ||
|
||
# ... and restrict the PLs in the old room, if possible. | ||
old_room_pl_state = yield self.state_handler.get_current_state( | ||
old_room_id, | ||
event_type=EventTypes.PowerLevels, | ||
latest_event_ids=(tombstone_event.event_id, ), | ||
) | ||
richvdh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if old_room_pl_state is None: | ||
logger.warning( | ||
"Not supported: upgrading a room with no PL event. Not setting PLs " | ||
"in old room.", | ||
) | ||
else: | ||
pl_content = dict(old_room_pl_state.content) | ||
users_default = int(pl_content.get("users_default", 0)) | ||
restricted_level = max(users_default + 1, 50) | ||
richvdh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
updated = False | ||
for v in ("invite", "events_default"): | ||
current = int(pl_content.get(v, 0)) | ||
if current < restricted_level: | ||
logger.debug( | ||
"Setting level for %s in %s to %i (was %i)", | ||
v, old_room_id, restricted_level, current, | ||
) | ||
pl_content[v] = restricted_level | ||
updated = True | ||
else: | ||
logger.debug( | ||
"Not setting level for %s (already %i)", | ||
v, current, | ||
) | ||
|
||
if updated: | ||
yield self.event_creation_handler.create_and_send_nonmember_event( | ||
requester, { | ||
"type": EventTypes.PowerLevels, | ||
"state_key": '', | ||
"room_id": old_room_id, | ||
"sender": user_id, | ||
"content": pl_content, | ||
}, ratelimit=False, | ||
) | ||
|
||
defer.returnValue(new_room_id) | ||
|
||
@defer.inlineCallbacks | ||
def clone_exiting_room( | ||
self, requester, old_room_id, new_room_id, new_room_version, | ||
tombstone_event_id, | ||
): | ||
"""Populate a new room based on an old room | ||
|
||
Args: | ||
requester (synapse.types.Requester): the user requesting the upgrade | ||
old_room_id (unicode): the id of the room to be replaced | ||
new_room_id (unicode): the id to give the new room (should already have been | ||
created with _gemerate_room_id()) | ||
new_room_version (unicode): the new room version to use | ||
tombstone_event_id (unicode|str): the ID of the tombstone event in the old | ||
room. | ||
Returns: | ||
Deferred[None] | ||
""" | ||
user_id = requester.user.to_string() | ||
|
||
if not self.spam_checker.user_may_create_room(user_id): | ||
raise SynapseError(403, "You are not permitted to create rooms") | ||
|
||
# XXX check alias is free | ||
# canonical_alias = None | ||
|
||
# XXX create association in directory handler | ||
|
||
creation_content = { | ||
"room_version": new_room_version, | ||
"predecessor": { | ||
"room_id": old_room_id, | ||
"event_id": tombstone_event_id, | ||
} | ||
} | ||
|
||
initial_state = dict() | ||
|
||
types_to_copy = ( | ||
(EventTypes.PowerLevels, ""), | ||
(EventTypes.JoinRules, ""), | ||
(EventTypes.Name, ""), | ||
(EventTypes.Topic, ""), | ||
(EventTypes.RoomHistoryVisibility, ""), | ||
(EventTypes.GuestAccess, "") | ||
) | ||
|
||
old_room_state_ids = yield self.store.get_filtered_current_state_ids( | ||
old_room_id, StateFilter.from_types(types_to_copy), | ||
) | ||
# map from event_id to BaseEvent | ||
old_room_state_events = yield self.store.get_events(old_room_state_ids.values()) | ||
|
||
for k in types_to_copy: | ||
old_event_id = old_room_state_ids.get(k) | ||
if old_event_id: | ||
richvdh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
old_event = old_room_state_events.get(old_event_id) | ||
if old_event: | ||
initial_state[k] = old_event.content | ||
|
||
yield self._send_events_for_new_room( | ||
requester, | ||
new_room_id, | ||
|
||
# we expect to override all the presets with initial_state, so this is | ||
# somewhat arbitrary. | ||
preset_config=RoomCreationPreset.PRIVATE_CHAT, | ||
|
||
invite_list=[], | ||
initial_state=initial_state, | ||
creation_content=creation_content, | ||
) | ||
|
||
# XXX invites/joins | ||
# XXX 3pid invites | ||
# XXX directory_handler.send_room_alias_update_event | ||
|
||
@defer.inlineCallbacks | ||
def create_room(self, requester, config, ratelimit=True, | ||
|
@@ -165,28 +350,7 @@ def create_room(self, requester, config, ratelimit=True, | |
visibility = config.get("visibility", None) | ||
is_public = visibility == "public" | ||
|
||
# autogen room IDs and try to create it. We may clash, so just | ||
# try a few times till one goes through, giving up eventually. | ||
attempts = 0 | ||
room_id = None | ||
while attempts < 5: | ||
try: | ||
random_string = stringutils.random_string(18) | ||
gen_room_id = RoomID( | ||
random_string, | ||
self.hs.hostname, | ||
) | ||
yield self.store.store_room( | ||
room_id=gen_room_id.to_string(), | ||
room_creator_user_id=user_id, | ||
is_public=is_public | ||
) | ||
room_id = gen_room_id.to_string() | ||
break | ||
except StoreError: | ||
attempts += 1 | ||
if not room_id: | ||
raise StoreError(500, "Couldn't generate a room ID.") | ||
room_id = yield self._generate_room_id(creator_id=user_id, is_public=is_public) | ||
|
||
if room_alias: | ||
directory_handler = self.hs.get_handlers().directory_handler | ||
|
@@ -216,18 +380,15 @@ def create_room(self, requester, config, ratelimit=True, | |
# override any attempt to set room versions via the creation_content | ||
creation_content["room_version"] = room_version | ||
|
||
room_member_handler = self.hs.get_room_member_handler() | ||
|
||
yield self._send_events_for_new_room( | ||
requester, | ||
room_id, | ||
room_member_handler, | ||
preset_config=preset_config, | ||
invite_list=invite_list, | ||
initial_state=initial_state, | ||
creation_content=creation_content, | ||
room_alias=room_alias, | ||
power_level_content_override=config.get("power_level_content_override", {}), | ||
power_level_content_override=config.get("power_level_content_override"), | ||
creator_join_profile=creator_join_profile, | ||
) | ||
|
||
|
@@ -263,7 +424,7 @@ def create_room(self, requester, config, ratelimit=True, | |
if is_direct: | ||
content["is_direct"] = is_direct | ||
|
||
yield room_member_handler.update_membership( | ||
yield self.room_member_handler.update_membership( | ||
requester, | ||
UserID.from_string(invitee), | ||
room_id, | ||
|
@@ -301,14 +462,13 @@ def _send_events_for_new_room( | |
self, | ||
creator, # A Requester object. | ||
room_id, | ||
room_member_handler, | ||
preset_config, | ||
invite_list, | ||
initial_state, | ||
creation_content, | ||
room_alias, | ||
power_level_content_override, | ||
creator_join_profile, | ||
room_alias=None, | ||
power_level_content_override=None, | ||
creator_join_profile=None, | ||
): | ||
def create(etype, content, **kwargs): | ||
e = { | ||
|
@@ -346,7 +506,7 @@ def send(etype, content, **kwargs): | |
content=creation_content, | ||
) | ||
|
||
yield room_member_handler.update_membership( | ||
yield self.room_member_handler.update_membership( | ||
creator, | ||
creator.user, | ||
room_id, | ||
|
@@ -388,7 +548,8 @@ def send(etype, content, **kwargs): | |
for invitee in invite_list: | ||
power_level_content["users"][invitee] = 100 | ||
|
||
power_level_content.update(power_level_content_override) | ||
if power_level_content_override: | ||
power_level_content.update(power_level_content_override) | ||
|
||
yield send( | ||
etype=EventTypes.PowerLevels, | ||
|
@@ -427,6 +588,30 @@ def send(etype, content, **kwargs): | |
content=content, | ||
) | ||
|
||
@defer.inlineCallbacks | ||
def _generate_room_id(self, creator_id, is_public): | ||
# autogen room IDs and try to create it. We may clash, so just | ||
# try a few times till one goes through, giving up eventually. | ||
attempts = 0 | ||
while attempts < 5: | ||
try: | ||
random_string = stringutils.random_string(18) | ||
gen_room_id = RoomID( | ||
random_string, | ||
self.hs.hostname, | ||
).to_string() | ||
if isinstance(gen_room_id, bytes): | ||
gen_room_id = gen_room_id.decode('utf-8') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How on earth can we end up with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
yield self.store.store_room( | ||
room_id=gen_room_id, | ||
room_creator_user_id=creator_id, | ||
is_public=is_public, | ||
) | ||
defer.returnValue(gen_room_id) | ||
except StoreError: | ||
attempts += 1 | ||
raise StoreError(500, "Couldn't generate a room ID.") | ||
|
||
|
||
class RoomContextHandler(object): | ||
def __init__(self, hs): | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't suppose we could somehow be more helpful here? Like add a matrix.to link? IDK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, I guess? The intention is that the client can interpret the tombstone event anyway.