2014-08-12 16:10:52 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2016-01-07 05:26:29 +01:00
|
|
|
# Copyright 2014-2016 OpenMarket Ltd
|
2014-08-12 16:10:52 +02: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-13 04:14:34 +02:00
|
|
|
|
2018-07-09 08:09:20 +02:00
|
|
|
import logging
|
|
|
|
import string
|
|
|
|
|
2014-08-12 16:10:52 +02:00
|
|
|
from twisted.internet import defer
|
|
|
|
|
2014-12-16 12:29:05 +01:00
|
|
|
from synapse.api.constants import EventTypes
|
2018-07-09 08:09:20 +02:00
|
|
|
from synapse.api.errors import AuthError, CodeMessageException, Codes, SynapseError
|
2016-08-26 15:54:30 +02:00
|
|
|
from synapse.types import RoomAlias, UserID, get_domain_from_id
|
2014-08-12 16:10:52 +02:00
|
|
|
|
2018-07-09 08:09:20 +02:00
|
|
|
from ._base import BaseHandler
|
2014-08-12 16:10:52 +02:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class DirectoryHandler(BaseHandler):
|
|
|
|
|
|
|
|
def __init__(self, hs):
|
|
|
|
super(DirectoryHandler, self).__init__(hs)
|
2014-08-13 19:03:37 +02:00
|
|
|
|
2016-03-17 12:09:03 +01:00
|
|
|
self.state = hs.get_state_handler()
|
2016-05-31 14:53:48 +02:00
|
|
|
self.appservice_handler = hs.get_application_service_handler()
|
2018-01-15 17:52:07 +01:00
|
|
|
self.event_creation_handler = hs.get_event_creation_handler()
|
2016-03-17 12:09:03 +01:00
|
|
|
|
2018-03-13 14:26:52 +01:00
|
|
|
self.federation = hs.get_federation_client()
|
2018-03-12 17:17:08 +01:00
|
|
|
hs.get_federation_registry().register_query_handler(
|
2014-08-13 19:03:37 +02:00
|
|
|
"directory", self.on_directory_query
|
|
|
|
)
|
2014-08-12 16:10:52 +02:00
|
|
|
|
2017-10-04 11:47:54 +02:00
|
|
|
self.spam_checker = hs.get_spam_checker()
|
|
|
|
|
2014-08-12 16:10:52 +02:00
|
|
|
@defer.inlineCallbacks
|
2016-03-01 15:46:31 +01:00
|
|
|
def _create_association(self, room_alias, room_id, servers=None, creator=None):
|
2015-02-06 11:57:14 +01:00
|
|
|
# general association creation for both human users and app services
|
2014-08-12 16:10:52 +02:00
|
|
|
|
2015-05-14 14:21:55 +02:00
|
|
|
for wchar in string.whitespace:
|
|
|
|
if wchar in room_alias.localpart:
|
|
|
|
raise SynapseError(400, "Invalid characters in room alias")
|
|
|
|
|
2014-12-02 11:42:28 +01:00
|
|
|
if not self.hs.is_mine(room_alias):
|
2014-08-12 16:10:52 +02:00
|
|
|
raise SynapseError(400, "Room alias must be local")
|
|
|
|
# TODO(erikj): Change this.
|
|
|
|
|
|
|
|
# TODO(erikj): Add transactions.
|
|
|
|
# TODO(erikj): Check if there is a current association.
|
2014-09-03 17:04:21 +02:00
|
|
|
if not servers:
|
2016-08-26 15:54:30 +02:00
|
|
|
users = yield self.state.get_current_user_in_room(room_id)
|
|
|
|
servers = set(get_domain_from_id(u) for u in users)
|
2014-09-03 17:04:21 +02:00
|
|
|
|
|
|
|
if not servers:
|
|
|
|
raise SynapseError(400, "Failed to get server list")
|
|
|
|
|
2014-11-18 16:03:01 +01:00
|
|
|
yield self.store.create_room_alias_association(
|
|
|
|
room_alias,
|
|
|
|
room_id,
|
2016-03-01 15:46:31 +01:00
|
|
|
servers,
|
|
|
|
creator=creator,
|
2014-11-18 16:03:01 +01:00
|
|
|
)
|
2014-09-05 22:35:56 +02:00
|
|
|
|
2015-02-06 11:57:14 +01:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def create_association(self, user_id, room_alias, room_id, servers=None):
|
|
|
|
# association creation for human users
|
|
|
|
# TODO(erikj): Do user auth.
|
|
|
|
|
2017-10-04 11:47:54 +02:00
|
|
|
if not self.spam_checker.user_may_create_room_alias(user_id, room_alias):
|
|
|
|
raise SynapseError(
|
|
|
|
403, "This user is not permitted to create this alias",
|
|
|
|
)
|
|
|
|
|
2015-02-09 16:01:28 +01:00
|
|
|
can_create = yield self.can_modify_alias(
|
|
|
|
room_alias,
|
|
|
|
user_id=user_id
|
|
|
|
)
|
|
|
|
if not can_create:
|
2015-02-06 11:57:14 +01:00
|
|
|
raise SynapseError(
|
|
|
|
400, "This alias is reserved by an application service.",
|
|
|
|
errcode=Codes.EXCLUSIVE
|
|
|
|
)
|
2016-03-01 15:46:31 +01:00
|
|
|
yield self._create_association(room_alias, room_id, servers, creator=user_id)
|
2015-02-06 11:57:14 +01:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def create_appservice_association(self, service, room_alias, room_id,
|
|
|
|
servers=None):
|
|
|
|
if not service.is_interested_in_alias(room_alias.to_string()):
|
|
|
|
raise SynapseError(
|
|
|
|
400, "This application service has not reserved"
|
|
|
|
" this kind of alias.", errcode=Codes.EXCLUSIVE
|
|
|
|
)
|
|
|
|
|
|
|
|
# association creation for app services
|
|
|
|
yield self._create_association(room_alias, room_id, servers)
|
|
|
|
|
2014-09-30 12:31:22 +02:00
|
|
|
@defer.inlineCallbacks
|
2016-03-17 12:09:03 +01:00
|
|
|
def delete_association(self, requester, user_id, room_alias):
|
2015-02-06 12:32:07 +01:00
|
|
|
# association deletion for human users
|
2014-09-05 22:35:56 +02:00
|
|
|
|
2016-03-01 15:46:31 +01:00
|
|
|
can_delete = yield self._user_can_delete_alias(room_alias, user_id)
|
|
|
|
if not can_delete:
|
|
|
|
raise AuthError(
|
|
|
|
403, "You don't have permission to delete the alias.",
|
|
|
|
)
|
2014-08-12 16:10:52 +02:00
|
|
|
|
2015-02-09 16:01:28 +01:00
|
|
|
can_delete = yield self.can_modify_alias(
|
|
|
|
room_alias,
|
|
|
|
user_id=user_id
|
|
|
|
)
|
|
|
|
if not can_delete:
|
2015-02-05 17:29:56 +01:00
|
|
|
raise SynapseError(
|
2015-02-06 11:57:14 +01:00
|
|
|
400, "This alias is reserved by an application service.",
|
|
|
|
errcode=Codes.EXCLUSIVE
|
2015-02-05 17:29:56 +01:00
|
|
|
)
|
|
|
|
|
2016-03-17 12:09:03 +01:00
|
|
|
room_id = yield self._delete_association(room_alias)
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield self.send_room_alias_update_event(
|
|
|
|
requester,
|
|
|
|
requester.user.to_string(),
|
|
|
|
room_id
|
|
|
|
)
|
|
|
|
|
|
|
|
yield self._update_canonical_alias(
|
|
|
|
requester,
|
|
|
|
requester.user.to_string(),
|
|
|
|
room_id,
|
|
|
|
room_alias,
|
|
|
|
)
|
|
|
|
except AuthError as e:
|
|
|
|
logger.info("Failed to update alias events: %s", e)
|
|
|
|
|
|
|
|
defer.returnValue(room_id)
|
2015-02-06 12:32:07 +01:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def delete_appservice_association(self, service, room_alias):
|
|
|
|
if not service.is_interested_in_alias(room_alias.to_string()):
|
|
|
|
raise SynapseError(
|
|
|
|
400,
|
|
|
|
"This application service has not reserved this kind of alias",
|
|
|
|
errcode=Codes.EXCLUSIVE
|
|
|
|
)
|
|
|
|
yield self._delete_association(room_alias)
|
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def _delete_association(self, room_alias):
|
|
|
|
if not self.hs.is_mine(room_alias):
|
|
|
|
raise SynapseError(400, "Room alias must be local")
|
|
|
|
|
2016-03-17 12:09:03 +01:00
|
|
|
room_id = yield self.store.delete_room_alias(room_alias)
|
2014-09-05 22:35:56 +02:00
|
|
|
|
2016-03-17 12:09:03 +01:00
|
|
|
defer.returnValue(room_id)
|
2015-02-06 12:32:07 +01:00
|
|
|
|
2014-08-12 16:10:52 +02:00
|
|
|
@defer.inlineCallbacks
|
2014-08-13 19:03:37 +02:00
|
|
|
def get_association(self, room_alias):
|
2014-08-12 16:10:52 +02:00
|
|
|
room_id = None
|
2014-12-02 11:42:28 +01:00
|
|
|
if self.hs.is_mine(room_alias):
|
2015-02-05 17:29:56 +01:00
|
|
|
result = yield self.get_association_from_room_alias(
|
2014-08-12 16:10:52 +02:00
|
|
|
room_alias
|
|
|
|
)
|
|
|
|
|
|
|
|
if result:
|
|
|
|
room_id = result.room_id
|
|
|
|
servers = result.servers
|
2014-08-13 19:03:37 +02:00
|
|
|
else:
|
2014-11-21 16:11:48 +01:00
|
|
|
try:
|
|
|
|
result = yield self.federation.make_query(
|
|
|
|
destination=room_alias.domain,
|
|
|
|
query_type="directory",
|
|
|
|
args={
|
|
|
|
"room_alias": room_alias.to_string(),
|
|
|
|
},
|
|
|
|
retry_on_dns_fail=False,
|
2017-03-23 12:10:36 +01:00
|
|
|
ignore_backoff=True,
|
2014-11-21 16:11:48 +01:00
|
|
|
)
|
|
|
|
except CodeMessageException as e:
|
|
|
|
logging.warn("Error retrieving alias")
|
|
|
|
if e.code == 404:
|
|
|
|
result = None
|
|
|
|
else:
|
|
|
|
raise
|
2014-08-12 16:10:52 +02:00
|
|
|
|
|
|
|
if result and "room_id" in result and "servers" in result:
|
|
|
|
room_id = result["room_id"]
|
|
|
|
servers = result["servers"]
|
|
|
|
|
|
|
|
if not room_id:
|
2014-11-21 16:11:48 +01:00
|
|
|
raise SynapseError(
|
|
|
|
404,
|
2015-02-24 16:00:12 +01:00
|
|
|
"Room alias %s not found" % (room_alias.to_string(),),
|
2014-11-21 16:11:48 +01:00
|
|
|
Codes.NOT_FOUND
|
|
|
|
)
|
2014-08-12 16:10:52 +02:00
|
|
|
|
2016-08-26 15:54:30 +02:00
|
|
|
users = yield self.state.get_current_user_in_room(room_id)
|
|
|
|
extra_servers = set(get_domain_from_id(u) for u in users)
|
2015-02-04 16:02:23 +01:00
|
|
|
servers = set(extra_servers) | set(servers)
|
|
|
|
|
|
|
|
# If this server is in the list of servers, return it first.
|
|
|
|
if self.server_name in servers:
|
|
|
|
servers = (
|
2016-02-02 18:18:50 +01:00
|
|
|
[self.server_name] +
|
|
|
|
[s for s in servers if s != self.server_name]
|
2015-02-04 16:02:23 +01:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
servers = list(servers)
|
2014-09-03 17:04:21 +02:00
|
|
|
|
2014-08-12 16:10:52 +02:00
|
|
|
defer.returnValue({
|
|
|
|
"room_id": room_id,
|
|
|
|
"servers": servers,
|
|
|
|
})
|
|
|
|
return
|
2014-08-13 19:03:37 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def on_directory_query(self, args):
|
2015-01-23 14:21:58 +01:00
|
|
|
room_alias = RoomAlias.from_string(args["room_alias"])
|
2014-12-02 11:42:28 +01:00
|
|
|
if not self.hs.is_mine(room_alias):
|
2014-08-13 19:03:37 +02:00
|
|
|
raise SynapseError(
|
|
|
|
400, "Room Alias is not hosted on this Home Server"
|
|
|
|
)
|
|
|
|
|
2015-02-05 17:29:56 +01:00
|
|
|
result = yield self.get_association_from_room_alias(
|
2014-08-13 19:03:37 +02:00
|
|
|
room_alias
|
|
|
|
)
|
|
|
|
|
2014-11-19 18:14:14 +01:00
|
|
|
if result is not None:
|
|
|
|
defer.returnValue({
|
|
|
|
"room_id": result.room_id,
|
|
|
|
"servers": result.servers,
|
|
|
|
})
|
|
|
|
else:
|
2014-11-20 18:26:36 +01:00
|
|
|
raise SynapseError(
|
2014-11-21 16:11:48 +01:00
|
|
|
404,
|
|
|
|
"Room alias %r not found" % (room_alias.to_string(),),
|
|
|
|
Codes.NOT_FOUND
|
2014-11-20 18:26:36 +01:00
|
|
|
)
|
2014-09-30 12:31:22 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
2016-03-03 17:43:42 +01:00
|
|
|
def send_room_alias_update_event(self, requester, user_id, room_id):
|
2014-09-30 12:31:22 +02:00
|
|
|
aliases = yield self.store.get_aliases_for_room(room_id)
|
|
|
|
|
2018-01-15 17:52:07 +01:00
|
|
|
yield self.event_creation_handler.create_and_send_nonmember_event(
|
2016-03-03 17:43:42 +01:00
|
|
|
requester,
|
|
|
|
{
|
|
|
|
"type": EventTypes.Aliases,
|
|
|
|
"state_key": self.hs.hostname,
|
|
|
|
"room_id": room_id,
|
|
|
|
"sender": user_id,
|
|
|
|
"content": {"aliases": aliases},
|
|
|
|
},
|
|
|
|
ratelimit=False
|
|
|
|
)
|
2015-02-05 17:29:56 +01:00
|
|
|
|
2016-03-17 12:09:03 +01:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def _update_canonical_alias(self, requester, user_id, room_id, room_alias):
|
|
|
|
alias_event = yield self.state.get_current_state(
|
|
|
|
room_id, EventTypes.CanonicalAlias, ""
|
|
|
|
)
|
|
|
|
|
2016-03-17 16:24:19 +01:00
|
|
|
alias_str = room_alias.to_string()
|
|
|
|
if not alias_event or alias_event.content.get("alias", "") != alias_str:
|
2016-03-17 12:09:03 +01:00
|
|
|
return
|
|
|
|
|
2018-01-15 17:52:07 +01:00
|
|
|
yield self.event_creation_handler.create_and_send_nonmember_event(
|
2016-03-17 12:09:03 +01:00
|
|
|
requester,
|
|
|
|
{
|
|
|
|
"type": EventTypes.CanonicalAlias,
|
|
|
|
"state_key": "",
|
|
|
|
"room_id": room_id,
|
|
|
|
"sender": user_id,
|
|
|
|
"content": {},
|
|
|
|
},
|
|
|
|
ratelimit=False
|
|
|
|
)
|
|
|
|
|
2015-02-05 17:29:56 +01:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def get_association_from_room_alias(self, room_alias):
|
|
|
|
result = yield self.store.get_association_from_room_alias(
|
|
|
|
room_alias
|
|
|
|
)
|
|
|
|
if not result:
|
|
|
|
# Query AS to see if it exists
|
2016-05-31 14:53:48 +02:00
|
|
|
as_handler = self.appservice_handler
|
2015-02-05 17:29:56 +01:00
|
|
|
result = yield as_handler.query_room_alias_exists(room_alias)
|
|
|
|
defer.returnValue(result)
|
|
|
|
|
2015-02-09 16:01:28 +01:00
|
|
|
def can_modify_alias(self, alias, user_id=None):
|
2015-02-27 14:51:41 +01:00
|
|
|
# Any application service "interested" in an alias they are regexing on
|
|
|
|
# can modify the alias.
|
|
|
|
# Users can only modify the alias if ALL the interested services have
|
|
|
|
# non-exclusive locks on the alias (or there are no interested services)
|
2016-10-06 10:43:32 +02:00
|
|
|
services = self.store.get_app_services()
|
2015-02-05 17:29:56 +01:00
|
|
|
interested_services = [
|
|
|
|
s for s in services if s.is_interested_in_alias(alias.to_string())
|
|
|
|
]
|
2015-02-27 14:51:41 +01:00
|
|
|
|
2015-02-09 16:01:28 +01:00
|
|
|
for service in interested_services:
|
|
|
|
if user_id == service.sender:
|
2015-02-27 14:51:41 +01:00
|
|
|
# this user IS the app service so they can do whatever they like
|
2016-10-06 10:43:32 +02:00
|
|
|
return defer.succeed(True)
|
2015-02-27 14:51:41 +01:00
|
|
|
elif service.is_exclusive_alias(alias.to_string()):
|
|
|
|
# another service has an exclusive lock on this alias.
|
2016-10-06 10:43:32 +02:00
|
|
|
return defer.succeed(False)
|
2015-02-27 14:51:41 +01:00
|
|
|
# either no interested services, or no service with an exclusive lock
|
2016-10-06 10:43:32 +02:00
|
|
|
return defer.succeed(True)
|
2016-03-01 15:46:31 +01:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def _user_can_delete_alias(self, alias, user_id):
|
|
|
|
creator = yield self.store.get_room_alias_creator(alias.to_string())
|
|
|
|
|
|
|
|
if creator and creator == user_id:
|
|
|
|
defer.returnValue(True)
|
|
|
|
|
|
|
|
is_admin = yield self.auth.is_server_admin(UserID.from_string(user_id))
|
|
|
|
defer.returnValue(is_admin)
|
2016-03-21 15:03:20 +01:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def edit_published_room_list(self, requester, room_id, visibility):
|
2016-03-22 11:41:44 +01:00
|
|
|
"""Edit the entry of the room in the published room list.
|
|
|
|
|
|
|
|
requester
|
|
|
|
room_id (str)
|
|
|
|
visibility (str): "public" or "private"
|
|
|
|
"""
|
2017-10-04 15:29:33 +02:00
|
|
|
if not self.spam_checker.user_may_publish_room(
|
|
|
|
requester.user.to_string(), room_id
|
|
|
|
):
|
|
|
|
raise AuthError(
|
|
|
|
403,
|
|
|
|
"This user is not permitted to publish rooms to the room list"
|
|
|
|
)
|
|
|
|
|
2016-03-21 15:03:20 +01:00
|
|
|
if requester.is_guest:
|
|
|
|
raise AuthError(403, "Guests cannot edit the published room list")
|
|
|
|
|
|
|
|
if visibility not in ["public", "private"]:
|
2016-03-22 01:52:31 +01:00
|
|
|
raise SynapseError(400, "Invalid visibility setting")
|
2016-03-21 15:03:20 +01:00
|
|
|
|
|
|
|
room = yield self.store.get_room(room_id)
|
|
|
|
if room is None:
|
|
|
|
raise SynapseError(400, "Unknown room")
|
|
|
|
|
|
|
|
yield self.auth.check_can_change_room_list(room_id, requester.user)
|
|
|
|
|
|
|
|
yield self.store.set_room_is_public(room_id, visibility == "public")
|
2016-12-06 11:43:48 +01:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
|
|
|
def edit_published_appservice_room_list(self, appservice_id, network_id,
|
|
|
|
room_id, visibility):
|
2016-12-07 10:58:33 +01:00
|
|
|
"""Add or remove a room from the appservice/network specific public
|
|
|
|
room list.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
appservice_id (str): ID of the appservice that owns the list
|
|
|
|
network_id (str): The ID of the network the list is associated with
|
|
|
|
room_id (str)
|
|
|
|
visibility (str): either "public" or "private"
|
2016-12-06 11:43:48 +01:00
|
|
|
"""
|
|
|
|
if visibility not in ["public", "private"]:
|
|
|
|
raise SynapseError(400, "Invalid visibility setting")
|
|
|
|
|
|
|
|
yield self.store.set_room_is_public_appservice(
|
|
|
|
room_id, appservice_id, network_id, visibility == "public"
|
|
|
|
)
|