Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Room version upgrade support #4091

Merged
merged 12 commits into from
Oct 29, 2018
1 change: 1 addition & 0 deletions changelog.d/4091.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for replacing rooms with new ones
1 change: 1 addition & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class LoginType(object):
class EventTypes(object):
Member = "m.room.member"
Create = "m.room.create"
Tombstone = "m.room.tombstone"
JoinRules = "m.room.join_rules"
PowerLevels = "m.room.power_levels"
Aliases = "m.room.aliases"
Expand Down
6 changes: 4 additions & 2 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def __init__(self, hs):
self._auth_handler = hs.get_auth_handler()
self.profile_handler = hs.get_profile_handler()
self.user_directory_handler = hs.get_user_directory_handler()
self.room_creation_handler = self.hs.get_room_creation_handler()
self.captcha_client = CaptchaServerHttpClient(hs)

self._next_generated_user_id = None
Expand Down Expand Up @@ -241,7 +240,10 @@ def register(
else:
# create room expects the localpart of the room alias
room_alias_localpart = room_alias.localpart
yield self.room_creation_handler.create_room(

# getting the RoomCreationHandler during init gives a dependency
# loop
yield self.hs.get_room_creation_handler().create_room(
fake_requester,
config={
"preset": "public_chat",
Expand Down
253 changes: 219 additions & 34 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Copy link
Member

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

Copy link
Member Author

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.

"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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How on earth can we end up with bytes here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DomainSpecificString.to_string returns a str, which is a bytes under python 2.

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):
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
register,
report_event,
room_keys,
room_upgrade_rest_servlet,
sendtodevice,
sync,
tags,
Expand Down Expand Up @@ -116,3 +117,4 @@ def register_servlets(client_resource, hs):
sendtodevice.register_servlets(hs, client_resource)
user_directory.register_servlets(hs, client_resource)
groups.register_servlets(hs, client_resource)
room_upgrade_rest_servlet.register_servlets(hs, client_resource)
Loading