synapse-product/synapse/handlers/room.py

726 lines
24 KiB
Python
Raw Normal View History

2014-08-12 10:10:52 -04:00
# -*- coding: utf-8 -*-
2016-01-05 13:01:18 -05:00
# Copyright 2014 - 2016 OpenMarket Ltd
2018-02-06 11:40:38 -05:00
# Copyright 2018 New Vector Ltd
2014-08-12 10:10:52 -04:00
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
2014-08-12 10:10:52 -04:00
"""Contains functions for performing events on rooms."""
import itertools
2018-07-09 02:09:20 -04:00
import logging
import math
import string
from collections import OrderedDict
2014-08-12 10:10:52 -04:00
from six import string_types
2018-07-09 02:09:20 -04:00
from twisted.internet import defer
from synapse.api.constants import (
DEFAULT_ROOM_VERSION,
KNOWN_ROOM_VERSIONS,
EventTypes,
JoinRules,
RoomCreationPreset,
)
2018-10-12 06:13:40 -04:00
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
2016-04-01 09:06:00 -04:00
from synapse.util import stringutils
from synapse.util.async_helpers import Linearizer
from synapse.visibility import filter_events_for_client
2014-08-12 10:10:52 -04:00
2018-07-09 02:09:20 -04:00
from ._base import BaseHandler
2014-08-12 10:10:52 -04:00
logger = logging.getLogger(__name__)
id_server_scheme = "https://"
2014-08-12 10:10:52 -04:00
class RoomCreationHandler(BaseHandler):
2014-08-12 10:10:52 -04:00
2015-07-13 11:48:06 -04:00
PRESETS_DICT = {
2015-07-14 05:20:31 -04:00
RoomCreationPreset.PRIVATE_CHAT: {
2015-07-13 11:48:06 -04:00
"join_rules": JoinRules.INVITE,
"history_visibility": "shared",
"original_invitees_have_ops": False,
"guest_can_join": True,
2015-07-13 11:48:06 -04:00
},
RoomCreationPreset.TRUSTED_PRIVATE_CHAT: {
"join_rules": JoinRules.INVITE,
"history_visibility": "shared",
"original_invitees_have_ops": True,
"guest_can_join": True,
},
2015-07-14 05:20:31 -04:00
RoomCreationPreset.PUBLIC_CHAT: {
2015-07-13 11:48:06 -04:00
"join_rules": JoinRules.PUBLIC,
"history_visibility": "shared",
"original_invitees_have_ops": False,
"guest_can_join": False,
2015-07-13 11:48:06 -04:00
},
}
def __init__(self, hs):
super(RoomCreationHandler, self).__init__(hs)
self.spam_checker = hs.get_spam_checker()
2018-01-15 11:52:07 -05:00
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
2018-10-12 06:13:40 -04:00
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(
2018-10-12 06:13:40 -04:00
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,
)
# XXX send a power_levels in the old room, if possible
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
# XXX preset
preset_config = RoomCreationPreset.PRIVATE_CHAT
creation_content = {
"room_version": new_room_version,
"predecessor": {
"room_id": old_room_id,
"event_id": tombstone_event_id,
}
}
initial_state = OrderedDict()
yield self._send_events_for_new_room(
requester,
new_room_id,
preset_config=preset_config,
invite_list=[],
initial_state=initial_state,
creation_content=creation_content,
)
# XXX name
# XXX topic
# XXX invites/joins
# XXX 3pid invites
# XXX directory_handler.send_room_alias_update_event
2014-08-12 10:10:52 -04:00
@defer.inlineCallbacks
def create_room(self, requester, config, ratelimit=True,
creator_join_profile=None):
2014-08-12 10:10:52 -04:00
""" Creates a new room.
Args:
requester (synapse.types.Requester):
The user who requested the room creation.
2014-08-12 10:10:52 -04:00
config (dict) : A dict of configuration options.
ratelimit (bool): set to False to disable the rate limiter
creator_join_profile (dict|None):
Set to override the displayname and avatar for the creating
user in this room. If unset, displayname and avatar will be
derived from the user's profile. If set, should contain the
values to go in the body of the 'join' event (typically
`avatar_url` and/or `displayname`.
2014-08-12 10:10:52 -04:00
Returns:
Deferred[dict]:
a dict containing the keys `room_id` and, if an alias was
requested, `room_alias`.
2014-08-12 10:10:52 -04:00
Raises:
2016-02-15 13:13:10 -05:00
SynapseError if the room ID couldn't be stored, or something went
horribly wrong.
ResourceLimitError if server is blocked to some resource being
exceeded
2014-08-12 10:10:52 -04:00
"""
2016-02-15 13:13:10 -05:00
user_id = requester.user.to_string()
self.auth.check_auth_blocking(user_id)
if not self.spam_checker.user_may_create_room(user_id):
2017-10-04 07:44:27 -04:00
raise SynapseError(403, "You are not permitted to create rooms")
if ratelimit:
yield self.ratelimit(requester)
2014-08-12 10:10:52 -04:00
room_version = config.get("room_version", DEFAULT_ROOM_VERSION)
if not isinstance(room_version, string_types):
raise SynapseError(
400,
"room_version must be a string",
Codes.BAD_JSON,
)
if room_version not in KNOWN_ROOM_VERSIONS:
raise SynapseError(
400,
"Your homeserver does not support this room version",
Codes.UNSUPPORTED_ROOM_VERSION,
)
2014-08-12 10:10:52 -04:00
if "room_alias_name" in config:
for wchar in string.whitespace:
if wchar in config["room_alias_name"]:
raise SynapseError(400, "Invalid characters in room alias")
room_alias = RoomAlias(
2014-08-12 10:10:52 -04:00
config["room_alias_name"],
self.hs.hostname,
2014-08-12 10:10:52 -04:00
)
mapping = yield self.store.get_association_from_room_alias(
room_alias
)
if mapping:
raise SynapseError(
400,
"Room alias already taken",
Codes.ROOM_IN_USE
)
2014-08-12 10:10:52 -04:00
else:
room_alias = None
invite_list = config.get("invite", [])
for i in invite_list:
try:
UserID.from_string(i)
except Exception:
raise SynapseError(400, "Invalid user_id: %s" % (i,))
yield self.event_creation_handler.assert_accepted_privacy_policy(
requester,
)
2016-01-05 06:56:21 -05:00
invite_3pid_list = config.get("invite_3pid", [])
visibility = config.get("visibility", None)
is_public = visibility == "public"
room_id = yield self._generate_room_id(creator_id=user_id, is_public=is_public)
2014-08-12 10:10:52 -04:00
if room_alias:
directory_handler = self.hs.get_handlers().directory_handler
yield directory_handler.create_association(
2018-10-18 11:14:24 -04:00
requester=requester,
room_id=room_id,
room_alias=room_alias,
servers=[self.hs.hostname],
2018-10-18 11:14:24 -04:00
send_event=False,
)
2015-07-13 11:48:06 -04:00
preset_config = config.get(
"preset",
RoomCreationPreset.PRIVATE_CHAT
if visibility == "private"
else RoomCreationPreset.PUBLIC_CHAT
2015-07-13 11:48:06 -04:00
)
raw_initial_state = config.get("initial_state", [])
initial_state = OrderedDict()
for val in raw_initial_state:
initial_state[(val["type"], val.get("state_key", ""))] = val["content"]
creation_content = config.get("creation_content", {})
# override any attempt to set room versions via the creation_content
creation_content["room_version"] = room_version
2016-02-16 07:00:50 -05:00
yield self._send_events_for_new_room(
requester,
room_id,
2015-07-13 11:48:06 -04:00
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"),
creator_join_profile=creator_join_profile,
)
if "name" in config:
name = config["name"]
2018-01-15 11:52:07 -05:00
yield self.event_creation_handler.create_and_send_nonmember_event(
requester,
{
"type": EventTypes.Name,
"room_id": room_id,
"sender": user_id,
"state_key": "",
"content": {"name": name},
},
ratelimit=False)
if "topic" in config:
topic = config["topic"]
2018-01-15 11:52:07 -05:00
yield self.event_creation_handler.create_and_send_nonmember_event(
requester,
{
"type": EventTypes.Topic,
"room_id": room_id,
"sender": user_id,
"state_key": "",
"content": {"topic": topic},
},
ratelimit=False)
for invitee in invite_list:
content = {}
is_direct = config.get("is_direct", None)
if is_direct:
content["is_direct"] = is_direct
2017-11-28 10:23:26 -05:00
yield self.room_member_handler.update_membership(
requester,
UserID.from_string(invitee),
room_id,
"invite",
ratelimit=False,
2016-09-12 11:34:20 -04:00
content=content,
)
2016-01-05 06:56:21 -05:00
for invite_3pid in invite_3pid_list:
id_server = invite_3pid["id_server"]
address = invite_3pid["address"]
medium = invite_3pid["medium"]
2018-03-01 05:54:37 -05:00
yield self.hs.get_room_member_handler().do_3pid_invite(
2016-01-05 06:56:21 -05:00
room_id,
2016-02-16 07:00:50 -05:00
requester.user,
2016-01-05 06:56:21 -05:00
medium,
address,
id_server,
requester,
2016-01-05 07:57:45 -05:00
txn_id=None,
2016-01-05 06:56:21 -05:00
)
2014-08-12 10:10:52 -04:00
result = {"room_id": room_id}
2014-08-12 10:10:52 -04:00
if room_alias:
result["room_alias"] = room_alias.to_string()
2014-11-20 12:26:36 -05:00
yield directory_handler.send_room_alias_update_event(
2018-10-18 11:14:24 -04:00
requester, room_id
2014-11-20 12:26:36 -05:00
)
2014-08-12 10:10:52 -04:00
defer.returnValue(result)
2016-02-16 07:00:50 -05:00
@defer.inlineCallbacks
def _send_events_for_new_room(
self,
creator, # A Requester object.
room_id,
preset_config,
invite_list,
initial_state,
creation_content,
room_alias=None,
power_level_content_override=None,
creator_join_profile=None,
2016-02-16 07:00:50 -05:00
):
2014-12-08 05:16:18 -05:00
def create(etype, content, **kwargs):
e = {
"type": etype,
"content": content,
}
e.update(event_keys)
2014-12-08 05:16:18 -05:00
e.update(kwargs)
return e
2014-09-01 11:15:34 -04:00
2016-02-16 07:00:50 -05:00
@defer.inlineCallbacks
def send(etype, content, **kwargs):
event = create(etype, content, **kwargs)
2018-01-15 11:52:07 -05:00
yield self.event_creation_handler.create_and_send_nonmember_event(
creator,
event,
ratelimit=False
)
2016-02-16 07:00:50 -05:00
config = RoomCreationHandler.PRESETS_DICT[preset_config]
creator_id = creator.user.to_string()
event_keys = {
"room_id": room_id,
"sender": creator_id,
"state_key": "",
}
creation_content.update({"creator": creator_id})
yield send(
2014-12-16 06:29:05 -05:00
etype=EventTypes.Create,
content=creation_content,
)
yield self.room_member_handler.update_membership(
2016-02-16 07:00:50 -05:00
creator,
creator.user,
room_id,
"join",
ratelimit=False,
content=creator_join_profile,
)
# We treat the power levels override specially as this needs to be one
# of the first events that get sent into a room.
pl_content = initial_state.pop((EventTypes.PowerLevels, ''), None)
if pl_content is not None:
yield send(
etype=EventTypes.PowerLevels,
content=pl_content,
)
else:
power_level_content = {
"users": {
2016-02-16 07:00:50 -05:00
creator_id: 100,
},
"users_default": 0,
"events": {
EventTypes.Name: 50,
EventTypes.PowerLevels: 100,
EventTypes.RoomHistoryVisibility: 100,
EventTypes.CanonicalAlias: 50,
EventTypes.RoomAvatar: 50,
},
"events_default": 0,
"state_default": 50,
"ban": 50,
"kick": 50,
"redact": 50,
"invite": 0,
}
2015-07-13 11:48:06 -04:00
if config["original_invitees_have_ops"]:
for invitee in invite_list:
power_level_content["users"][invitee] = 100
2015-07-13 11:48:06 -04:00
if power_level_content_override:
power_level_content.update(power_level_content_override)
2016-02-16 07:00:50 -05:00
yield send(
etype=EventTypes.PowerLevels,
content=power_level_content,
)
2015-09-30 11:46:24 -04:00
if room_alias and (EventTypes.CanonicalAlias, '') not in initial_state:
2016-02-16 07:00:50 -05:00
yield send(
2015-09-30 11:46:24 -04:00
etype=EventTypes.CanonicalAlias,
content={"alias": room_alias.to_string()},
)
if (EventTypes.JoinRules, '') not in initial_state:
2016-02-16 07:00:50 -05:00
yield send(
etype=EventTypes.JoinRules,
content={"join_rule": config["join_rules"]},
)
if (EventTypes.RoomHistoryVisibility, '') not in initial_state:
2016-02-16 07:00:50 -05:00
yield send(
etype=EventTypes.RoomHistoryVisibility,
content={"history_visibility": config["history_visibility"]}
)
if config["guest_can_join"]:
if (EventTypes.GuestAccess, '') not in initial_state:
yield send(
etype=EventTypes.GuestAccess,
content={"guest_access": "can_join"}
)
for (etype, state_key), content in initial_state.items():
2016-02-16 07:00:50 -05:00
yield send(
etype=etype,
state_key=state_key,
content=content,
2016-02-16 07:00:50 -05:00
)
@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')
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.")
2014-08-12 10:10:52 -04:00
class RoomContextHandler(object):
def __init__(self, hs):
self.hs = hs
self.store = hs.get_datastore()
2015-10-28 09:45:56 -04:00
@defer.inlineCallbacks
def get_event_context(self, user, room_id, event_id, limit, event_filter):
2015-10-28 10:05:50 -04:00
"""Retrieves events, pagination tokens and state around a given event
in a room.
Args:
user (UserID)
room_id (str)
event_id (str)
limit (int): The maximum number of events to return in total
(excluding state).
event_filter (Filter|None): the filter to apply to the events returned
(excluding the target event_id)
2015-10-28 10:05:50 -04:00
Returns:
dict, or None if the event isn't found
2015-10-28 10:05:50 -04:00
"""
2016-02-02 12:18:50 -05:00
before_limit = math.floor(limit / 2.)
2015-10-28 09:45:56 -04:00
after_limit = limit - before_limit
2017-02-20 09:54:50 -05:00
users = yield self.store.get_users_in_room(room_id)
is_peeking = user.to_string() not in users
def filter_evts(events):
return filter_events_for_client(
self.store,
user.to_string(),
events,
2017-02-20 09:54:50 -05:00
is_peeking=is_peeking
)
event = yield self.store.get_event(event_id, get_prev_content=True,
allow_none=True)
if not event:
defer.returnValue(None)
return
filtered = yield(filter_evts([event]))
if not filtered:
raise AuthError(
403,
"You don't have permission to access that event."
)
2015-10-28 09:45:56 -04:00
results = yield self.store.get_events_around(
room_id, event_id, before_limit, after_limit, event_filter
2015-10-28 09:45:56 -04:00
)
results["events_before"] = yield filter_evts(results["events_before"])
results["events_after"] = yield filter_evts(results["events_after"])
results["event"] = event
2015-10-28 09:45:56 -04:00
if results["events_after"]:
last_event_id = results["events_after"][-1].event_id
else:
last_event_id = event_id
if event_filter and event_filter.lazy_load_members():
state_filter = StateFilter.from_lazy_load_member_list(
ev.sender
for ev in itertools.chain(
results["events_before"],
(results["event"],),
results["events_after"],
)
)
else:
state_filter = StateFilter.all()
# XXX: why do we return the state as of the last event rather than the
# first? Shouldn't we be consistent with /sync?
# https://github.com/matrix-org/matrix-doc/issues/687
2015-10-28 09:45:56 -04:00
state = yield self.store.get_state_for_events(
[last_event_id], state_filter=state_filter,
2015-10-28 09:45:56 -04:00
)
results["state"] = list(state[last_event_id].values())
2015-10-28 09:45:56 -04:00
# We use a dummy token here as we only care about the room portion of
# the token, which we replace.
token = StreamToken.START
results["start"] = token.copy_and_replace(
2015-10-28 09:45:56 -04:00
"room_key", results["start"]
).to_string()
results["end"] = token.copy_and_replace(
2015-10-28 09:45:56 -04:00
"room_key", results["end"]
).to_string()
defer.returnValue(results)
class RoomEventSource(object):
def __init__(self, hs):
self.store = hs.get_datastore()
@defer.inlineCallbacks
def get_new_events(
self,
user,
from_key,
limit,
room_ids,
is_guest,
explicit_room_id=None,
):
# We just ignore the key for now.
to_key = yield self.get_current_key()
from_token = RoomStreamToken.parse(from_key)
if from_token.topological:
logger.warn("Stream has topological part!!!! %r", from_key)
from_key = "s%s" % (from_token.stream,)
app_service = self.store.get_app_service_by_user_id(
user.to_string()
)
if app_service:
# We no longer support AS users using /sync directly.
# See https://github.com/matrix-org/matrix-doc/issues/1144
raise NotImplementedError()
else:
room_events = yield self.store.get_membership_changes_for_user(
user.to_string(), from_key, to_key
)
room_to_events = yield self.store.get_room_events_stream_for_rooms(
room_ids=room_ids,
from_key=from_key,
to_key=to_key,
limit=limit or 10,
order='ASC',
)
events = list(room_events)
events.extend(e for evs, _ in room_to_events.values() for e in evs)
2016-02-01 11:32:46 -05:00
events.sort(key=lambda e: e.internal_metadata.order)
if limit:
events[:] = events[:limit]
if events:
end_key = events[-1].internal_metadata.after
else:
end_key = to_key
defer.returnValue((events, end_key))
def get_current_key(self):
return self.store.get_room_events_max_id()
def get_current_key_for_room(self, room_id):
return self.store.get_room_events_max_id(room_id)
@defer.inlineCallbacks
def get_pagination_rows(self, user, config, key):
events, next_key = yield self.store.paginate_room_events(
room_id=key,
from_key=config.from_key,
to_key=config.to_key,
direction=config.direction,
limit=config.limit,
)
defer.returnValue((events, next_key))