summary refs log tree commit diff
diff options
context:
space:
mode:
-rwxr-xr-xdemo/start.sh5
-rwxr-xr-xsynapse/app/homeserver.py10
-rw-r--r--synapse/handlers/presence.py9
-rw-r--r--synapse/rest/base.py2
-rw-r--r--synapse/rest/room.py70
-rw-r--r--synapse/rest/transactions.py96
-rw-r--r--synapse/storage/roommember.py21
-rw-r--r--webclient/components/matrix/matrix-service.js18
8 files changed, 217 insertions, 14 deletions
diff --git a/demo/start.sh b/demo/start.sh
index 4701872926..1e591aabb8 100755
--- a/demo/start.sh
+++ b/demo/start.sh
@@ -6,7 +6,7 @@ CWD=$(pwd)
 
 cd "$DIR/.."
 
-for port in "8080" "8081" "8082"; do
+for port in 8080 8081 8082; do
     echo "Starting server on port $port... "
 
     python -m synapse.app.homeserver \
@@ -15,7 +15,8 @@ for port in "8080" "8081" "8082"; do
         -f "$DIR/$port.log" \
         -d "$DIR/$port.db" \
         -vv \
-        -D --pid-file "$DIR/$port.pid"
+        -D --pid-file "$DIR/$port.pid" \
+        --manhole $((port + 1000))
 done
 
 echo "Starting webclient on port 8000..."
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 40e3561ee5..f210d26629 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -31,6 +31,7 @@ from synapse.api.urls import (
 )
 
 from daemonize import Daemonize
+import twisted.manhole.telnet
 
 import argparse
 import logging
@@ -238,6 +239,8 @@ def setup():
                         default="hs.pid")
     parser.add_argument("-W", "--webclient", dest="webclient", default=True,
                         action="store_false", help="Don't host a web client.")
+    parser.add_argument("--manhole", dest="manhole", type=int, default=None,
+                        help="Turn on the twisted telnet manhole service.")
     args = parser.parse_args()
 
     verbosity = int(args.verbose) if args.verbose else None
@@ -281,6 +284,13 @@ def setup():
 
     hs.build_db_pool()
 
+    if args.manhole:
+        f = twisted.manhole.telnet.ShellFactory()
+        f.username = "matrix"
+        f.password = "rabbithole"
+        f.namespace['hs'] = hs
+        reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
+
     if args.daemonize:
         daemon = Daemonize(
             app="synapse-homeserver",
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index c88cc18788..471dc86163 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -159,12 +159,11 @@ class PresenceHandler(BaseHandler):
         if allowed_by_subscription:
             defer.returnValue(True)
 
-        rm_handler = self.homeserver.get_handlers().room_member_handler
-        for room_id in (yield rm_handler.get_rooms_for_user(observer_user)):
-            if observed_user in (yield rm_handler.get_room_members(room_id)):
-                defer.returnValue(True)
+        share_room = yield self.store.do_users_share_a_room(
+            [observer_user, observed_user]
+        )
 
-        defer.returnValue(False)
+        defer.returnValue(share_room)
 
     @defer.inlineCallbacks
     def get_state(self, target_user, auth_user):
diff --git a/synapse/rest/base.py b/synapse/rest/base.py
index 6a88cbe866..e855d293e5 100644
--- a/synapse/rest/base.py
+++ b/synapse/rest/base.py
@@ -15,6 +15,7 @@
 
 """ This module contains base REST classes for constructing REST servlets. """
 from synapse.api.urls import CLIENT_PREFIX
+from synapse.rest.transactions import HttpTransactionStore
 import re
 
 
@@ -59,6 +60,7 @@ class RestServlet(object):
         self.handlers = hs.get_handlers()
         self.event_factory = hs.get_event_factory()
         self.auth = hs.get_auth()
+        self.txns = HttpTransactionStore()
 
     def register(self, http_server):
         """ Register this servlet with the given HTTP server. """
diff --git a/synapse/rest/room.py b/synapse/rest/room.py
index 2d681bd893..731317227d 100644
--- a/synapse/rest/room.py
+++ b/synapse/rest/room.py
@@ -366,6 +366,51 @@ class RoomTriggerBackfill(RestServlet):
         defer.returnValue((200, res))
 
 
+class RoomMembershipRestServlet(RestServlet):
+
+    def register(self, http_server):
+        # /rooms/$roomid/[invite|join|leave]
+        PATTERN = ("/rooms/(?P<room_id>[^/]*)/" +
+            "(?P<membership_action>join|invite|leave)")
+        register_txn_path(self, PATTERN, http_server)
+
+    @defer.inlineCallbacks
+    def on_POST(self, request, room_id, membership_action):
+        user = yield self.auth.get_user_by_req(request)
+
+        content = _parse_json(request)
+
+        # target user is you unless it is an invite
+        state_key = user.to_string()
+        if membership_action == "invite":
+            if "user_id" not in content:
+                raise SynapseError(400, "Missing user_id key.")
+            state_key = content["user_id"]
+
+        event = self.event_factory.create_event(
+            etype=RoomMemberEvent.TYPE,
+            content={"membership": unicode(membership_action)},
+            room_id=urllib.unquote(room_id),
+            user_id=user.to_string(),
+            state_key=state_key
+        )
+        handler = self.handlers.room_member_handler
+        yield handler.change_membership(event)
+        defer.returnValue((200, ""))
+
+    @defer.inlineCallbacks
+    def on_PUT(self, request, room_id, membership_action, txn_id):
+        try:
+            defer.returnValue(self.txns.get_client_transaction(request, txn_id))
+        except:
+            pass
+
+        response = yield self.on_POST(request, room_id, membership_action)
+
+        self.txns.store_client_transaction(request, txn_id, response)
+        defer.returnValue(response)
+
+
 def _parse_json(request):
     try:
         content = json.loads(request.content.read())
@@ -377,6 +422,30 @@ def _parse_json(request):
         raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
 
 
+def register_txn_path(servlet, regex_string, http_server):
+    """Registers a transaction-based path.
+
+    This registers two paths:
+        PUT regex_string/$txnid
+        POST regex_string
+
+    Args:
+        regex_string (str): The regex string to register. Must NOT have a
+        trailing $ as this string will be appended to.
+        http_server : The http_server to register paths with.
+    """
+    http_server.register_path(
+        "POST",
+        client_path_pattern(regex_string + "$"),
+        servlet.on_POST
+    )
+    http_server.register_path(
+        "PUT",
+        client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
+        servlet.on_PUT
+    )
+
+
 def register_servlets(hs, http_server):
     RoomStateEventRestServlet(hs).register(http_server)
     MessageRestServlet(hs).register(http_server)
@@ -386,3 +455,4 @@ def register_servlets(hs, http_server):
     RoomMessageListRestServlet(hs).register(http_server)
     JoinRoomAliasServlet(hs).register(http_server)
     RoomTriggerBackfill(hs).register(http_server)
+    RoomMembershipRestServlet(hs).register(http_server)
diff --git a/synapse/rest/transactions.py b/synapse/rest/transactions.py
new file mode 100644
index 0000000000..b8aa1ef11c
--- /dev/null
+++ b/synapse/rest/transactions.py
@@ -0,0 +1,96 @@
+# -*- 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 logic for storing HTTP PUT transactions. This is used
+to ensure idempotency when performing PUTs using the REST API."""
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class HttpTransactionStore(object):
+
+    def __init__(self):
+        # { key : (txn_id, response) }
+        self.transactions = {}
+
+    def get_response(self, key, txn_id):
+        """Retrieve a response for this request.
+
+        Args:
+            key (str): A transaction-independent key for this request. Typically
+            this is a combination of the path (without the transaction id) and
+            the user's access token.
+            txn_id (str): The transaction ID for this request
+        Returns:
+            A tuple of (HTTP response code, response content) or None.
+        """
+        try:
+            logger.debug("get_response Key: %s TxnId: %s", key, txn_id)
+            (last_txn_id, response) = self.transactions[key]
+            if txn_id == last_txn_id:
+                logger.info("get_response: Returning a response for %s", key)
+                return response
+        except KeyError:
+            pass
+        return None
+
+    def store_response(self, key, txn_id, response):
+        """Stores an HTTP response tuple.
+
+        Args:
+            key (str): A transaction-independent key for this request. Typically
+            this is a combination of the path (without the transaction id) and
+            the user's access token.
+            txn_id (str): The transaction ID for this request.
+            response (tuple): A tuple of (HTTP response code, response content)
+        """
+        logger.debug("store_response Key: %s TxnId: %s", key, txn_id)
+        self.transactions[key] = (txn_id, response)
+
+    def store_client_transaction(self, request, txn_id, response):
+        """Stores the request/response pair of an HTTP transaction.
+
+        Args:
+            request (twisted.web.http.Request): The twisted HTTP request. This
+            request must have the transaction ID as the last path segment.
+            response (tuple): A tuple of (response code, response dict)
+            txn_id (str): The transaction ID for this request.
+        """
+        self.store_response(self._get_key(request), txn_id, response)
+
+    def get_client_transaction(self, request, txn_id):
+        """Retrieves a stored response if there was one.
+
+        Args:
+            request (twisted.web.http.Request): The twisted HTTP request. This
+            request must have the transaction ID as the last path segment.
+            txn_id (str): The transaction ID for this request.
+        Returns:
+            The response tuple.
+        Raises:
+            KeyError if the transaction was not found.
+        """
+        response = self.get_response(self._get_key(request), txn_id)
+        if response is None:
+            raise KeyError("Transaction not found.")
+        return response
+
+    def _get_key(self, request):
+        token = request.args["access_token"][0]
+        path_without_txn_id = request.path.rsplit("/", 1)[0]
+        return path_without_txn_id + "/" + token
+
+
diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py
index a9a09e1425..86519b60c6 100644
--- a/synapse/storage/roommember.py
+++ b/synapse/storage/roommember.py
@@ -150,3 +150,24 @@ class RoomMemberStore(SQLBaseStore):
 
         results = [self._parse_event_from_row(r) for r in rows]
         defer.returnValue(results)
+
+    @defer.inlineCallbacks
+    def do_users_share_a_room(self, user_list):
+        """ Checks whether a list of users share a room.
+        """
+        user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_list))
+        sql = (
+            "SELECT m.room_id FROM room_memberships as m "
+            "INNER JOIN current_state_events as c "
+            "ON m.event_id = c.event_id "
+            "WHERE m.membership = 'join' "
+            "AND (%(clause)s) "
+            "GROUP BY m.room_id HAVING COUNT(m.room_id) = ?"
+        ) % {"clause": user_list_clause}
+
+        args = user_list
+        args.append(len(user_list))
+
+        rows = yield self._execute(None, sql, *args)
+
+        defer.returnValue(len(rows) > 0)
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index b5b1815cf9..d376724e40 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -115,7 +115,7 @@ angular.module('matrixService', [])
 
         // Joins a room
         join: function(room_id) {
-            return this.membershipChange(room_id, config.user_id, "join");
+            return this.membershipChange(room_id, undefined, "join");
         },
 
         joinAlias: function(room_alias) {
@@ -134,18 +134,22 @@ angular.module('matrixService', [])
 
         // Leaves a room
         leave: function(room_id) {
-            return this.membershipChange(room_id, config.user_id, "leave");
+            return this.membershipChange(room_id, undefined, "leave");
         },
 
         membershipChange: function(room_id, user_id, membershipValue) {
             // The REST path spec
-            var path = "/rooms/$room_id/state/m.room.member/$user_id";
+            var path = "/rooms/$room_id/$membership";
             path = path.replace("$room_id", encodeURIComponent(room_id));
-            path = path.replace("$user_id", encodeURIComponent(user_id));
+            path = path.replace("$membership", encodeURIComponent(membershipValue));
 
-            return doRequest("PUT", path, undefined, {
-                 membership: membershipValue
-            });
+            var data = {};
+            if (user_id !== undefined) {
+                data = { user_id: user_id };
+            }
+
+            // TODO: Use PUT with transaction IDs
+            return doRequest("POST", path, undefined, data);
         },
 
         // Retrieves the room ID corresponding to a room alias