summary refs log tree commit diff
path: root/synapse
diff options
context:
space:
mode:
Diffstat (limited to 'synapse')
-rw-r--r--synapse/api/auth.py7
-rw-r--r--synapse/handlers/_base.py10
-rw-r--r--synapse/handlers/room.py3
-rw-r--r--synapse/rest/client/v1/room.py13
-rw-r--r--synapse/storage/roommember.py66
-rw-r--r--synapse/storage/schema/delta/26/forgotten_memberships.sql24
6 files changed, 118 insertions, 5 deletions
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 4a13f7e2e1..4fdc779b4b 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -207,6 +207,13 @@ class Auth(object):
                 user_id, room_id
             ))
 
+        if membership == Membership.LEAVE:
+            forgot = yield self.store.did_forget(user_id, room_id)
+            if forgot:
+                raise AuthError(403, "User %s not in room %s" % (
+                    user_id, room_id
+                ))
+
         defer.returnValue(member)
 
     @defer.inlineCallbacks
diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py
index 6519f183df..5fd20285d2 100644
--- a/synapse/handlers/_base.py
+++ b/synapse/handlers/_base.py
@@ -92,7 +92,15 @@ class BaseHandler(object):
 
             membership_event = state.get((EventTypes.Member, user_id), None)
             if membership_event:
-                membership = membership_event.membership
+                was_forgotten_at_event = yield self.store.was_forgotten_at(
+                    membership_event.state_key,
+                    membership_event.room_id,
+                    membership_event.event_id
+                )
+                if was_forgotten_at_event:
+                    membership = None
+                else:
+                    membership = membership_event.membership
             else:
                 membership = None
 
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 3f04752581..023b4001b8 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -743,6 +743,9 @@ class RoomMemberHandler(BaseHandler):
         )
         defer.returnValue((token, public_key, key_validity_url, display_name))
 
+    def forget(self, user, room_id):
+        self.store.forget(user.to_string(), room_id)
+
 
 class RoomListHandler(BaseHandler):
 
diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py
index 139dac1cc3..6952d269ec 100644
--- a/synapse/rest/client/v1/room.py
+++ b/synapse/rest/client/v1/room.py
@@ -448,7 +448,7 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
     def register(self, http_server):
         # /rooms/$roomid/[invite|join|leave]
         PATTERN = ("/rooms/(?P<room_id>[^/]*)/"
-                   "(?P<membership_action>join|invite|leave|ban|kick)")
+                   "(?P<membership_action>join|invite|leave|ban|kick|forget)")
         register_txn_path(self, PATTERN, http_server)
 
     @defer.inlineCallbacks
@@ -458,6 +458,8 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
             allow_guest=True
         )
 
+        effective_membership_action = membership_action
+
         if is_guest and membership_action not in {Membership.JOIN, Membership.LEAVE}:
             raise AuthError(403, "Guest access not allowed")
 
@@ -488,11 +490,13 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
             UserID.from_string(state_key)
 
             if membership_action == "kick":
-                membership_action = "leave"
+                effective_membership_action = "leave"
+        elif membership_action == "forget":
+            effective_membership_action = "leave"
 
         msg_handler = self.handlers.message_handler
 
-        content = {"membership": unicode(membership_action)}
+        content = {"membership": unicode(effective_membership_action)}
         if is_guest:
             content["kind"] = "guest"
 
@@ -509,6 +513,9 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
             is_guest=is_guest,
         )
 
+        if membership_action == "forget":
+            self.handlers.room_member_handler.forget(user, room_id)
+
         defer.returnValue((200, {}))
 
     def _has_3pid_invite_keys(self, content):
diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py
index ae1ad56d9a..d32ce1ab1e 100644
--- a/synapse/storage/roommember.py
+++ b/synapse/storage/roommember.py
@@ -160,7 +160,7 @@ class RoomMemberStore(SQLBaseStore):
 
     def _get_rooms_for_user_where_membership_is_txn(self, txn, user_id,
                                                     membership_list):
-        where_clause = "user_id = ? AND (%s)" % (
+        where_clause = "user_id = ? AND (%s) AND NOT forgotten" % (
             " OR ".join(["membership = ?" for _ in membership_list]),
         )
 
@@ -269,3 +269,67 @@ class RoomMemberStore(SQLBaseStore):
         ret = len(room_id_lists.pop(0).intersection(*room_id_lists)) > 0
 
         defer.returnValue(ret)
+
+    def forget(self, user_id, room_id):
+        """Indicate that user_id wishes to discard history for room_id."""
+        def f(txn):
+            sql = (
+                "UPDATE"
+                "  room_memberships"
+                " SET"
+                "  forgotten = 1"
+                " WHERE"
+                "  user_id = ?"
+                " AND"
+                "  room_id = ?"
+            )
+            txn.execute(sql, (user_id, room_id))
+        self.runInteraction("forget_membership", f)
+
+    @defer.inlineCallbacks
+    def did_forget(self, user_id, room_id):
+        """Returns whether user_id has elected to discard history for room_id.
+
+        Returns False if they have since re-joined."""
+        def f(txn):
+            sql = (
+                "SELECT"
+                "  COUNT(*)"
+                " FROM"
+                "  room_memberships"
+                " WHERE"
+                "  user_id = ?"
+                " AND"
+                "  room_id = ?"
+                " AND"
+                "  forgotten = 0"
+            )
+            txn.execute(sql, (user_id, room_id))
+            rows = txn.fetchall()
+            return rows[0][0]
+        count = yield self.runInteraction("did_forget_membership", f)
+        defer.returnValue(count == 0)
+
+    @defer.inlineCallbacks
+    def was_forgotten_at(self, user_id, room_id, event_id):
+        """Returns whether user_id has elected to discard history for room_id at event_id.
+
+        event_id must be a membership event."""
+        def f(txn):
+            sql = (
+                "SELECT"
+                "  forgotten"
+                " FROM"
+                "  room_memberships"
+                " WHERE"
+                "  user_id = ?"
+                " AND"
+                "  room_id = ?"
+                " AND"
+                "  event_id = ?"
+            )
+            txn.execute(sql, (user_id, room_id, event_id))
+            rows = txn.fetchall()
+            return rows[0][0]
+        forgot = yield self.runInteraction("did_forget_membership_at", f)
+        defer.returnValue(forgot == 1)
diff --git a/synapse/storage/schema/delta/26/forgotten_memberships.sql b/synapse/storage/schema/delta/26/forgotten_memberships.sql
new file mode 100644
index 0000000000..df55b9c6f6
--- /dev/null
+++ b/synapse/storage/schema/delta/26/forgotten_memberships.sql
@@ -0,0 +1,24 @@
+/* Copyright 2015 OpenMarket Ltd
+ *
+ * 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.
+ */
+
+/*
+ * Keeps track of what rooms users have left and don't want to be able to
+ * access again.
+ *
+ * If all users on this server have left a room, we can delete the room
+ * entirely.
+ */
+
+ ALTER TABLE room_memberships ADD COLUMN forgotten INTEGER(1) DEFAULT 0;