diff options
author | matrix.org <matrix@matrix.org> | 2014-08-12 15:10:52 +0100 |
---|---|---|
committer | matrix.org <matrix@matrix.org> | 2014-08-12 15:10:52 +0100 |
commit | 4f475c7697722e946e39e42f38f3dd03a95d8765 (patch) | |
tree | 076d96d3809fb836c7245fd9f7960e7b75888a77 /synapse/rest | |
download | synapse-4f475c7697722e946e39e42f38f3dd03a95d8765.tar.xz |
Reference Matrix Home Server
Diffstat (limited to 'synapse/rest')
-rw-r--r-- | synapse/rest/__init__.py | 44 | ||||
-rw-r--r-- | synapse/rest/base.py | 113 | ||||
-rw-r--r-- | synapse/rest/directory.py | 82 | ||||
-rw-r--r-- | synapse/rest/events.py | 50 | ||||
-rw-r--r-- | synapse/rest/im.py | 39 | ||||
-rw-r--r-- | synapse/rest/login.py | 80 | ||||
-rw-r--r-- | synapse/rest/presence.py | 134 | ||||
-rw-r--r-- | synapse/rest/profile.py | 93 | ||||
-rw-r--r-- | synapse/rest/public.py | 32 | ||||
-rw-r--r-- | synapse/rest/register.py | 68 | ||||
-rw-r--r-- | synapse/rest/room.py | 394 |
11 files changed, 1129 insertions, 0 deletions
diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py new file mode 100644 index 0000000000..5598295793 --- /dev/null +++ b/synapse/rest/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. + +from . import ( + room, events, register, profile, public, presence, im, directory +) + +class RestServletFactory(object): + + """ A factory for creating REST servlets. + + These REST servlets represent the entire client-server REST API. Generally + speaking, they serve as wrappers around events and the handlers that + process them. + + See synapse.api.events for information on synapse events. + """ + + def __init__(self, hs): + http_server = hs.get_http_server() + + # TODO(erikj): There *must* be a better way of doing this. + room.register_servlets(hs, http_server) + events.register_servlets(hs, http_server) + register.register_servlets(hs, http_server) + profile.register_servlets(hs, http_server) + public.register_servlets(hs, http_server) + presence.register_servlets(hs, http_server) + im.register_servlets(hs, http_server) + directory.register_servlets(hs, http_server) + + diff --git a/synapse/rest/base.py b/synapse/rest/base.py new file mode 100644 index 0000000000..d90ac611fe --- /dev/null +++ b/synapse/rest/base.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +""" This module contains base REST classes for constructing REST servlets. """ +import re + + +def client_path_pattern(path_regex): + """Creates a regex compiled client path with the correct client path + prefix. + + Args: + path_regex (str): The regex string to match. This should NOT have a ^ + as this will be prefixed. + Returns: + SRE_Pattern + """ + return re.compile("^/matrix/client/api/v1" + path_regex) + + +class RestServletFactory(object): + + """ A factory for creating REST servlets. + + These REST servlets represent the entire client-server REST API. Generally + speaking, they serve as wrappers around events and the handlers that + process them. + + See synapse.api.events for information on synapse events. + """ + + def __init__(self, hs): + http_server = hs.get_http_server() + + # You get import errors if you try to import before the classes in this + # file are defined, hence importing here instead. + + import room + room.register_servlets(hs, http_server) + + import events + events.register_servlets(hs, http_server) + + import register + register.register_servlets(hs, http_server) + + import profile + profile.register_servlets(hs, http_server) + + import public + public.register_servlets(hs, http_server) + + import presence + presence.register_servlets(hs, http_server) + + import im + im.register_servlets(hs, http_server) + + import login + login.register_servlets(hs, http_server) + + +class RestServlet(object): + + """ A Synapse REST Servlet. + + An implementing class can either provide its own custom 'register' method, + or use the automatic pattern handling provided by the base class. + + To use this latter, the implementing class instead provides a `PATTERN` + class attribute containing a pre-compiled regular expression. The automatic + register method will then use this method to register any of the following + instance methods associated with the corresponding HTTP method: + + on_GET + on_PUT + on_POST + on_DELETE + on_OPTIONS + + Automatically handles turning CodeMessageExceptions thrown by these methods + into the appropriate HTTP response. + """ + + def __init__(self, hs): + self.hs = hs + + self.handlers = hs.get_handlers() + self.event_factory = hs.get_event_factory() + self.auth = hs.get_auth() + + def register(self, http_server): + """ Register this servlet with the given HTTP server. """ + if hasattr(self, "PATTERN"): + pattern = self.PATTERN + + for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"): + if hasattr(self, "on_%s" % (method)): + method_handler = getattr(self, "on_%s" % (method)) + http_server.register_path(method, pattern, method_handler) + else: + raise NotImplementedError("RestServlet must register something.") diff --git a/synapse/rest/directory.py b/synapse/rest/directory.py new file mode 100644 index 0000000000..a426003a38 --- /dev/null +++ b/synapse/rest/directory.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. + +from twisted.internet import defer + +from synapse.types import RoomAlias, RoomID +from base import RestServlet, client_path_pattern + +import json +import logging +import urllib + + +logger = logging.getLogger(__name__) + + +def register_servlets(hs, http_server): + ClientDirectoryServer(hs).register(http_server) + + +class ClientDirectoryServer(RestServlet): + PATTERN = client_path_pattern("/ds/room/(?P<room_alias>[^/]*)$") + + @defer.inlineCallbacks + def on_GET(self, request, room_alias): + # TODO(erikj): Handle request + local_only = "local_only" in request.args + + room_alias = urllib.unquote(room_alias) + room_alias_obj = RoomAlias.from_string(room_alias, self.hs) + + dir_handler = self.handlers.directory_handler + res = yield dir_handler.get_association( + room_alias_obj, + local_only=local_only + ) + + defer.returnValue((200, res)) + + @defer.inlineCallbacks + def on_PUT(self, request, room_alias): + # TODO(erikj): Exceptions + content = json.loads(request.content.read()) + + logger.debug("Got content: %s", content) + + room_alias = urllib.unquote(room_alias) + room_alias_obj = RoomAlias.from_string(room_alias, self.hs) + + logger.debug("Got room name: %s", room_alias_obj.to_string()) + + room_id = content["room_id"] + servers = content["servers"] + + logger.debug("Got room_id: %s", room_id) + logger.debug("Got servers: %s", servers) + + # TODO(erikj): Check types. + # TODO(erikj): Check that room exists + + dir_handler = self.handlers.directory_handler + + try: + yield dir_handler.create_association( + room_alias_obj, room_id, servers + ) + except: + logger.exception("Failed to create association") + + defer.returnValue((200, {})) diff --git a/synapse/rest/events.py b/synapse/rest/events.py new file mode 100644 index 0000000000..147257a940 --- /dev/null +++ b/synapse/rest/events.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +"""This module contains REST servlets to do with event streaming, /events.""" +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from synapse.api.streams import PaginationConfig +from synapse.rest.base import RestServlet, client_path_pattern + + +class EventStreamRestServlet(RestServlet): + PATTERN = client_path_pattern("/events$") + + DEFAULT_LONGPOLL_TIME_MS = 5000 + + @defer.inlineCallbacks + def on_GET(self, request): + auth_user = yield self.auth.get_user_by_req(request) + + handler = self.handlers.event_stream_handler + pagin_config = PaginationConfig.from_request(request) + timeout = EventStreamRestServlet.DEFAULT_LONGPOLL_TIME_MS + if "timeout" in request.args: + try: + timeout = int(request.args["timeout"][0]) + except ValueError: + raise SynapseError(400, "timeout must be in milliseconds.") + + chunk = yield handler.get_stream(auth_user.to_string(), pagin_config, + timeout=timeout) + defer.returnValue((200, chunk)) + + def on_OPTIONS(self, request): + return (200, {}) + + +def register_servlets(hs, http_server): + EventStreamRestServlet(hs).register(http_server) diff --git a/synapse/rest/im.py b/synapse/rest/im.py new file mode 100644 index 0000000000..39f2dbd749 --- /dev/null +++ b/synapse/rest/im.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +from twisted.internet import defer + +from synapse.api.streams import PaginationConfig +from base import RestServlet, client_path_pattern + + +class ImSyncRestServlet(RestServlet): + PATTERN = client_path_pattern("/im/sync$") + + @defer.inlineCallbacks + def on_GET(self, request): + user = yield self.auth.get_user_by_req(request) + with_feedback = "feedback" in request.args + pagination_config = PaginationConfig.from_request(request) + handler = self.handlers.message_handler + content = yield handler.snapshot_all_rooms( + user_id=user.to_string(), + pagin_config=pagination_config, + feedback=with_feedback) + + defer.returnValue((200, content)) + + +def register_servlets(hs, http_server): + ImSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/login.py b/synapse/rest/login.py new file mode 100644 index 0000000000..0284e125b4 --- /dev/null +++ b/synapse/rest/login.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from base import RestServlet, client_path_pattern + +import json + + +class LoginRestServlet(RestServlet): + PATTERN = client_path_pattern("/login$") + PASS_TYPE = "m.login.password" + + def on_GET(self, request): + return (200, {"type": LoginRestServlet.PASS_TYPE}) + + def on_OPTIONS(self, request): + return (200, {}) + + @defer.inlineCallbacks + def on_POST(self, request): + login_submission = _parse_json(request) + try: + if login_submission["type"] == LoginRestServlet.PASS_TYPE: + result = yield self.do_password_login(login_submission) + defer.returnValue(result) + else: + raise SynapseError(400, "Bad login type.") + except KeyError: + raise SynapseError(400, "Missing JSON keys.") + + @defer.inlineCallbacks + def do_password_login(self, login_submission): + handler = self.handlers.login_handler + token = yield handler.login( + user=login_submission["user"], + password=login_submission["password"]) + + result = { + "access_token": token, + "home_server": self.hs.hostname, + } + + defer.returnValue((200, result)) + + +class LoginFallbackRestServlet(RestServlet): + PATTERN = client_path_pattern("/login/fallback$") + + def on_GET(self, request): + # TODO(kegan): This should be returning some HTML which is capable of + # hitting LoginRestServlet + return (200, "") + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.") + return content + except ValueError: + raise SynapseError(400, "Content not JSON.") + + +def register_servlets(hs, http_server): + LoginRestServlet(hs).register(http_server) diff --git a/synapse/rest/presence.py b/synapse/rest/presence.py new file mode 100644 index 0000000000..e4925c20a5 --- /dev/null +++ b/synapse/rest/presence.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +""" This module contains REST servlets to do with presence: /presence/<paths> +""" +from twisted.internet import defer + +from base import RestServlet, client_path_pattern + +import json +import logging + + +logger = logging.getLogger(__name__) + + +class PresenceStatusRestServlet(RestServlet): + PATTERN = client_path_pattern("/presence/(?P<user_id>[^/]*)/status") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + state = yield self.handlers.presence_handler.get_state( + target_user=user, auth_user=auth_user) + + defer.returnValue((200, state)) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + state = {} + try: + content = json.loads(request.content.read()) + + state["state"] = content.pop("state") + + if "status_msg" in content: + state["status_msg"] = content.pop("status_msg") + + if content: + raise KeyError() + except: + defer.returnValue((400, "Unable to parse state")) + + yield self.handlers.presence_handler.set_state( + target_user=user, auth_user=auth_user, state=state) + + defer.returnValue((200, "")) + + def on_OPTIONS(self, request): + return (200, {}) + + +class PresenceListRestServlet(RestServlet): + PATTERN = client_path_pattern("/presence_list/(?P<user_id>[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + if not user.is_mine: + defer.returnValue((400, "User not hosted on this Home Server")) + + if auth_user != user: + defer.returnValue((400, "Cannot get another user's presence list")) + + presence = yield self.handlers.presence_handler.get_presence_list( + observer_user=user, accepted=True) + + for p in presence: + observed_user = p.pop("observed_user") + p["user_id"] = observed_user.to_string() + + defer.returnValue((200, presence)) + + @defer.inlineCallbacks + def on_POST(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + if not user.is_mine: + defer.returnValue((400, "User not hosted on this Home Server")) + + if auth_user != user: + defer.returnValue(( + 400, "Cannot modify another user's presence list")) + + try: + content = json.loads(request.content.read()) + except: + logger.exception("JSON parse error") + defer.returnValue((400, "Unable to parse content")) + + deferreds = [] + + if "invite" in content: + for u in content["invite"]: + invited_user = self.hs.parse_userid(u) + deferreds.append(self.handlers.presence_handler.send_invite( + observer_user=user, observed_user=invited_user)) + + if "drop" in content: + for u in content["drop"]: + dropped_user = self.hs.parse_userid(u) + deferreds.append(self.handlers.presence_handler.drop( + observer_user=user, observed_user=dropped_user)) + + yield defer.DeferredList(deferreds) + + defer.returnValue((200, "")) + + def on_OPTIONS(self, request): + return (200, {}) + + +def register_servlets(hs, http_server): + PresenceStatusRestServlet(hs).register(http_server) + PresenceListRestServlet(hs).register(http_server) diff --git a/synapse/rest/profile.py b/synapse/rest/profile.py new file mode 100644 index 0000000000..f384227c29 --- /dev/null +++ b/synapse/rest/profile.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +""" This module contains REST servlets to do with profile: /profile/<paths> """ +from twisted.internet import defer + +from base import RestServlet, client_path_pattern + +import json + + +class ProfileDisplaynameRestServlet(RestServlet): + PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)/displayname") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + user = self.hs.parse_userid(user_id) + + displayname = yield self.handlers.profile_handler.get_displayname( + user, + local_only="local_only" in request.args + ) + + defer.returnValue((200, {"displayname": displayname})) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + try: + content = json.loads(request.content.read()) + new_name = content["displayname"] + except: + defer.returnValue((400, "Unable to parse name")) + + yield self.handlers.profile_handler.set_displayname( + user, auth_user, new_name) + + defer.returnValue((200, "")) + + def on_OPTIONS(self, request, user_id): + return (200, {}) + + +class ProfileAvatarURLRestServlet(RestServlet): + PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)/avatar_url") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + user = self.hs.parse_userid(user_id) + + avatar_url = yield self.handlers.profile_handler.get_avatar_url( + user, + local_only="local_only" in request.args + ) + + defer.returnValue((200, {"avatar_url": avatar_url})) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + try: + content = json.loads(request.content.read()) + new_name = content["avatar_url"] + except: + defer.returnValue((400, "Unable to parse name")) + + yield self.handlers.profile_handler.set_avatar_url( + user, auth_user, new_name) + + defer.returnValue((200, "")) + + def on_OPTIONS(self, request, user_id): + return (200, {}) + + +def register_servlets(hs, http_server): + ProfileDisplaynameRestServlet(hs).register(http_server) + ProfileAvatarURLRestServlet(hs).register(http_server) diff --git a/synapse/rest/public.py b/synapse/rest/public.py new file mode 100644 index 0000000000..6fd1731a61 --- /dev/null +++ b/synapse/rest/public.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +"""This module contains REST servlets to do with public paths: /public""" +from twisted.internet import defer + +from base import RestServlet, client_path_pattern + + +class PublicRoomListRestServlet(RestServlet): + PATTERN = client_path_pattern("/public/rooms$") + + @defer.inlineCallbacks + def on_GET(self, request): + handler = self.handlers.room_list_handler + data = yield handler.get_public_room_list() + defer.returnValue((200, data)) + + +def register_servlets(hs, http_server): + PublicRoomListRestServlet(hs).register(http_server) diff --git a/synapse/rest/register.py b/synapse/rest/register.py new file mode 100644 index 0000000000..f1cbce5c67 --- /dev/null +++ b/synapse/rest/register.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +"""This module contains REST servlets to do with registration: /register""" +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from base import RestServlet, client_path_pattern + +import json +import urllib + + +class RegisterRestServlet(RestServlet): + PATTERN = client_path_pattern("/register$") + + @defer.inlineCallbacks + def on_POST(self, request): + desired_user_id = None + password = None + try: + register_json = json.loads(request.content.read()) + if "password" in register_json: + password = register_json["password"] + + if type(register_json["user_id"]) == unicode: + desired_user_id = register_json["user_id"] + if urllib.quote(desired_user_id) != desired_user_id: + raise SynapseError( + 400, + "User ID must only contain characters which do not " + + "require URL encoding.") + except ValueError: + defer.returnValue((400, "No JSON object.")) + except KeyError: + pass # user_id is optional + + handler = self.handlers.registration_handler + (user_id, token) = yield handler.register( + localpart=desired_user_id, + password=password) + + result = { + "user_id": user_id, + "access_token": token, + "home_server": self.hs.hostname, + } + defer.returnValue( + (200, result) + ) + + def on_OPTIONS(self, request): + return (200, {}) + + +def register_servlets(hs, http_server): + RegisterRestServlet(hs).register(http_server) diff --git a/synapse/rest/room.py b/synapse/rest/room.py new file mode 100644 index 0000000000..c96de5e65d --- /dev/null +++ b/synapse/rest/room.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 matrix.org +# +# 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. +""" This module contains REST servlets to do with rooms: /rooms/<paths> """ +from twisted.internet import defer + +from base import RestServlet, client_path_pattern +from synapse.api.errors import SynapseError, Codes +from synapse.api.events.room import (RoomTopicEvent, MessageEvent, + RoomMemberEvent, FeedbackEvent) +from synapse.api.constants import Feedback, Membership +from synapse.api.streams import PaginationConfig +from synapse.types import RoomAlias + +import json +import logging +import urllib + + +logger = logging.getLogger(__name__) + + +class RoomCreateRestServlet(RestServlet): + # No PATTERN; we have custom dispatch rules here + + def register(self, http_server): + # /rooms OR /rooms/<roomid> + http_server.register_path("POST", + client_path_pattern("/rooms$"), + self.on_POST) + http_server.register_path("PUT", + client_path_pattern( + "/rooms/(?P<room_id>[^/]*)$"), + self.on_PUT) + # define CORS for all of /rooms in RoomCreateRestServlet for simplicity + http_server.register_path("OPTIONS", + client_path_pattern("/rooms(?:/.*)?$"), + self.on_OPTIONS) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id): + room_id = urllib.unquote(room_id) + auth_user = yield self.auth.get_user_by_req(request) + + if not room_id: + raise SynapseError(400, "PUT must specify a room ID") + + room_config = self.get_room_config(request) + info = yield self.make_room(room_config, auth_user, room_id) + room_config.update(info) + defer.returnValue((200, info)) + + @defer.inlineCallbacks + def on_POST(self, request): + auth_user = yield self.auth.get_user_by_req(request) + + room_config = self.get_room_config(request) + info = yield self.make_room(room_config, auth_user, None) + room_config.update(info) + defer.returnValue((200, info)) + + @defer.inlineCallbacks + def make_room(self, room_config, auth_user, room_id): + handler = self.handlers.room_creation_handler + info = yield handler.create_room( + user_id=auth_user.to_string(), + room_id=room_id, + config=room_config + ) + defer.returnValue(info) + + def get_room_config(self, request): + try: + user_supplied_config = json.loads(request.content.read()) + if "visibility" not in user_supplied_config: + # default visibility + user_supplied_config["visibility"] = "public" + return user_supplied_config + except (ValueError, TypeError): + raise SynapseError(400, "Body must be JSON.", + errcode=Codes.BAD_JSON) + + def on_OPTIONS(self, request): + return (200, {}) + + +class RoomTopicRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/topic$") + + def get_event_type(self): + return RoomTopicEvent.TYPE + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + + msg_handler = self.handlers.message_handler + data = yield msg_handler.get_room_data( + user_id=user.to_string(), + room_id=urllib.unquote(room_id), + event_type=RoomTopicEvent.TYPE, + state_key="", + ) + + if not data: + raise SynapseError(404, "Topic not found.", errcode=Codes.NOT_FOUND) + defer.returnValue((200, json.loads(data.content))) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + + content = _parse_json(request) + + event = self.event_factory.create_event( + etype=self.get_event_type(), + content=content, + room_id=urllib.unquote(room_id), + user_id=user.to_string(), + ) + + msg_handler = self.handlers.message_handler + yield msg_handler.store_room_data( + event=event + ) + defer.returnValue((200, "")) + + +class JoinRoomAliasServlet(RestServlet): + PATTERN = client_path_pattern("/join/(?P<room_alias>[^/]+)$") + + @defer.inlineCallbacks + def on_PUT(self, request, room_alias): + user = yield self.auth.get_user_by_req(request) + + if not user: + defer.returnValue((403, "Unrecognized user")) + + logger.debug("room_alias: %s", room_alias) + + room_alias = RoomAlias.from_string( + urllib.unquote(room_alias), + self.hs + ) + + handler = self.handlers.room_member_handler + ret_dict = yield handler.join_room_alias(user, room_alias) + + defer.returnValue((200, ret_dict)) + + +class RoomMemberRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members/" + + "(?P<target_user_id>[^/]*)/state$") + + def get_event_type(self): + return RoomMemberEvent.TYPE + + @defer.inlineCallbacks + def on_GET(self, request, room_id, target_user_id): + room_id = urllib.unquote(room_id) + user = yield self.auth.get_user_by_req(request) + + handler = self.handlers.room_member_handler + member = yield handler.get_room_member(room_id, target_user_id, + user.to_string()) + if not member: + raise SynapseError(404, "Member not found.", + errcode=Codes.NOT_FOUND) + defer.returnValue((200, json.loads(member.content))) + + @defer.inlineCallbacks + def on_DELETE(self, request, roomid, target_user_id): + user = yield self.auth.get_user_by_req(request) + + event = self.event_factory.create_event( + etype=self.get_event_type(), + target_user_id=target_user_id, + room_id=urllib.unquote(roomid), + user_id=user.to_string(), + membership=Membership.LEAVE, + content={"membership": Membership.LEAVE} + ) + + handler = self.handlers.room_member_handler + yield handler.change_membership(event, broadcast_msg=True) + defer.returnValue((200, "")) + + @defer.inlineCallbacks + def on_PUT(self, request, roomid, target_user_id): + user = yield self.auth.get_user_by_req(request) + + content = _parse_json(request) + if "membership" not in content: + raise SynapseError(400, "No membership key.", + errcode=Codes.BAD_JSON) + + valid_membership_values = [Membership.JOIN, Membership.INVITE] + if (content["membership"] not in valid_membership_values): + raise SynapseError(400, "Membership value must be %s." % ( + valid_membership_values,), errcode=Codes.BAD_JSON) + + event = self.event_factory.create_event( + etype=self.get_event_type(), + target_user_id=target_user_id, + room_id=urllib.unquote(roomid), + user_id=user.to_string(), + membership=content["membership"], + content=content + ) + + handler = self.handlers.room_member_handler + result = yield handler.change_membership(event, broadcast_msg=True) + defer.returnValue((200, result)) + + +class MessageRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages/" + + "(?P<sender_id>[^/]*)/(?P<msg_id>[^/]*)$") + + def get_event_type(self): + return MessageEvent.TYPE + + @defer.inlineCallbacks + def on_GET(self, request, room_id, sender_id, msg_id): + user = yield self.auth.get_user_by_req(request) + + msg_handler = self.handlers.message_handler + msg = yield msg_handler.get_message(room_id=urllib.unquote(room_id), + sender_id=sender_id, + msg_id=msg_id, + user_id=user.to_string(), + ) + + if not msg: + raise SynapseError(404, "Message not found.", + errcode=Codes.NOT_FOUND) + + defer.returnValue((200, json.loads(msg.content))) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, sender_id, msg_id): + user = yield self.auth.get_user_by_req(request) + + if user.to_string() != sender_id: + raise SynapseError(403, "Must send messages as yourself.", + errcode=Codes.FORBIDDEN) + + content = _parse_json(request) + + event = self.event_factory.create_event( + etype=self.get_event_type(), + room_id=urllib.unquote(room_id), + user_id=user.to_string(), + msg_id=msg_id, + content=content + ) + + msg_handler = self.handlers.message_handler + yield msg_handler.send_message(event) + + defer.returnValue((200, "")) + + +class FeedbackRestServlet(RestServlet): + PATTERN = client_path_pattern( + "/rooms/(?P<room_id>[^/]*)/messages/" + + "(?P<msg_sender_id>[^/]*)/(?P<msg_id>[^/]*)/feedback/" + + "(?P<sender_id>[^/]*)/(?P<feedback_type>[^/]*)$" + ) + + def get_event_type(self): + return FeedbackEvent.TYPE + + @defer.inlineCallbacks + def on_GET(self, request, room_id, msg_sender_id, msg_id, fb_sender_id, + feedback_type): + user = yield (self.auth.get_user_by_req(request)) + + if feedback_type not in Feedback.LIST: + raise SynapseError(400, "Bad feedback type.", + errcode=Codes.BAD_JSON) + + msg_handler = self.handlers.message_handler + feedback = yield msg_handler.get_feedback( + room_id=urllib.unquote(room_id), + msg_sender_id=msg_sender_id, + msg_id=msg_id, + user_id=user.to_string(), + fb_sender_id=fb_sender_id, + fb_type=feedback_type + ) + + if not feedback: + raise SynapseError(404, "Feedback not found.", + errcode=Codes.NOT_FOUND) + + defer.returnValue((200, json.loads(feedback.content))) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, sender_id, msg_id, fb_sender_id, + feedback_type): + user = yield (self.auth.get_user_by_req(request)) + + if user.to_string() != fb_sender_id: + raise SynapseError(403, "Must send feedback as yourself.", + errcode=Codes.FORBIDDEN) + + if feedback_type not in Feedback.LIST: + raise SynapseError(400, "Bad feedback type.", + errcode=Codes.BAD_JSON) + + content = _parse_json(request) + + event = self.event_factory.create_event( + etype=self.get_event_type(), + room_id=urllib.unquote(room_id), + msg_sender_id=sender_id, + msg_id=msg_id, + user_id=user.to_string(), # user sending the feedback + feedback_type=feedback_type, + content=content + ) + + msg_handler = self.handlers.message_handler + yield msg_handler.send_feedback(event) + + defer.returnValue((200, "")) + + +class RoomMemberListRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members/list$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + # TODO support Pagination stream API (limit/tokens) + user = yield self.auth.get_user_by_req(request) + handler = self.handlers.room_member_handler + members = yield handler.get_room_members_as_pagination_chunk( + room_id=urllib.unquote(room_id), + user_id=user.to_string()) + + defer.returnValue((200, members)) + + +class RoomMessageListRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages/list$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + pagination_config = PaginationConfig.from_request(request) + with_feedback = "feedback" in request.args + handler = self.handlers.message_handler + msgs = yield handler.get_messages( + room_id=urllib.unquote(room_id), + user_id=user.to_string(), + pagin_config=pagination_config, + feedback=with_feedback) + + defer.returnValue((200, msgs)) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.", + errcode=Codes.NOT_JSON) + return content + except ValueError: + raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) + + +def register_servlets(hs, http_server): + RoomTopicRestServlet(hs).register(http_server) + RoomMemberRestServlet(hs).register(http_server) + MessageRestServlet(hs).register(http_server) + FeedbackRestServlet(hs).register(http_server) + RoomCreateRestServlet(hs).register(http_server) + RoomMemberListRestServlet(hs).register(http_server) + RoomMessageListRestServlet(hs).register(http_server) + JoinRoomAliasServlet(hs).register(http_server) |