summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--UPGRADE.rst5
-rwxr-xr-xcmdclient/console.py14
-rw-r--r--cmdclient/http.py2
-rwxr-xr-xdatabase-prepare-for-0.2.0.sh10
-rw-r--r--docs/client-server/OLD_specification.rst (renamed from docs/client-server/specification.rst)14
-rw-r--r--docs/client-server/swagger_matrix/api-docs-directory4
-rw-r--r--docs/client-server/swagger_matrix/api-docs-events2
-rw-r--r--docs/client-server/swagger_matrix/api-docs-login24
-rw-r--r--docs/client-server/swagger_matrix/api-docs-presence8
-rw-r--r--docs/client-server/swagger_matrix/api-docs-profile2
-rw-r--r--docs/client-server/swagger_matrix/api-docs-registration6
-rw-r--r--docs/client-server/swagger_matrix/api-docs-rooms114
-rw-r--r--docs/server-server/security-threat-model.rst141
-rw-r--r--docs/specification.rst604
-rw-r--r--experiments/cursesio.py2
-rw-r--r--experiments/test_messaging.py2
-rw-r--r--graph/graph.py2
-rwxr-xr-xscripts/copyrighter.pl4
-rwxr-xr-xscripts/gendoc.sh4
-rwxr-xr-xsetup.py2
-rw-r--r--synapse/__init__.py2
-rw-r--r--synapse/api/__init__.py2
-rw-r--r--synapse/api/auth.py2
-rw-r--r--synapse/api/constants.py2
-rw-r--r--synapse/api/errors.py2
-rw-r--r--synapse/api/events/__init__.py2
-rw-r--r--synapse/api/events/factory.py2
-rw-r--r--synapse/api/events/room.py5
-rw-r--r--synapse/api/ratelimiting.py2
-rw-r--r--synapse/api/urls.py2
-rw-r--r--synapse/app/__init__.py2
-rwxr-xr-xsynapse/app/homeserver.py32
-rw-r--r--synapse/config/__init__.py2
-rw-r--r--synapse/config/_base.py2
-rw-r--r--synapse/config/database.py2
-rw-r--r--synapse/config/homeserver.py5
-rw-r--r--synapse/config/logger.py2
-rw-r--r--synapse/config/ratelimiting.py2
-rw-r--r--synapse/config/repository.py39
-rw-r--r--synapse/config/server.py16
-rw-r--r--synapse/config/tls.py2
-rw-r--r--synapse/crypto/__init__.py2
-rw-r--r--synapse/crypto/context_factory.py2
-rw-r--r--synapse/crypto/keyclient.py2
-rw-r--r--synapse/crypto/keyserver.py2
-rw-r--r--synapse/crypto/resource/__init__.py2
-rw-r--r--synapse/crypto/resource/key.py2
-rw-r--r--synapse/federation/__init__.py2
-rw-r--r--synapse/federation/pdu_codec.py2
-rw-r--r--synapse/federation/persistence.py2
-rw-r--r--synapse/federation/replication.py3
-rw-r--r--synapse/federation/transport.py2
-rw-r--r--synapse/federation/units.py2
-rw-r--r--synapse/handlers/__init__.py2
-rw-r--r--synapse/handlers/_base.py2
-rw-r--r--synapse/handlers/directory.py19
-rw-r--r--synapse/handlers/events.py6
-rw-r--r--synapse/handlers/federation.py2
-rw-r--r--synapse/handlers/login.py2
-rw-r--r--synapse/handlers/message.py9
-rw-r--r--synapse/handlers/presence.py118
-rw-r--r--synapse/handlers/profile.py2
-rw-r--r--synapse/handlers/register.py2
-rw-r--r--synapse/handlers/room.py2
-rw-r--r--synapse/handlers/typing.py2
-rw-r--r--synapse/http/__init__.py2
-rw-r--r--synapse/http/client.py26
-rw-r--r--synapse/http/content_repository.py206
-rw-r--r--synapse/http/endpoint.py2
-rw-r--r--synapse/http/server.py165
-rw-r--r--synapse/notifier.py9
-rw-r--r--synapse/rest/__init__.py2
-rw-r--r--synapse/rest/base.py2
-rw-r--r--synapse/rest/directory.py24
-rw-r--r--synapse/rest/events.py2
-rw-r--r--synapse/rest/initial_sync.py2
-rw-r--r--synapse/rest/login.py2
-rw-r--r--synapse/rest/presence.py41
-rw-r--r--synapse/rest/profile.py8
-rw-r--r--synapse/rest/register.py2
-rw-r--r--synapse/rest/room.py9
-rw-r--r--synapse/rest/transactions.py2
-rw-r--r--synapse/server.py2
-rw-r--r--synapse/state.py14
-rw-r--r--synapse/storage/__init__.py2
-rw-r--r--synapse/storage/_base.py12
-rw-r--r--synapse/storage/directory.py2
-rw-r--r--synapse/storage/feedback.py2
-rw-r--r--synapse/storage/keys.py2
-rw-r--r--synapse/storage/pdu.py2
-rw-r--r--synapse/storage/presence.py2
-rw-r--r--synapse/storage/profile.py2
-rw-r--r--synapse/storage/registration.py2
-rw-r--r--synapse/storage/room.py2
-rw-r--r--synapse/storage/roommember.py4
-rw-r--r--synapse/storage/schema/delta/v2.sql168
-rw-r--r--synapse/storage/schema/edge_pdus.sql2
-rw-r--r--synapse/storage/schema/im.sql2
-rw-r--r--synapse/storage/schema/keys.sql2
-rw-r--r--synapse/storage/schema/pdu.sql2
-rw-r--r--synapse/storage/schema/presence.sql2
-rw-r--r--synapse/storage/schema/profiles.sql2
-rw-r--r--synapse/storage/schema/room_aliases.sql2
-rw-r--r--synapse/storage/schema/transactions.sql2
-rw-r--r--synapse/storage/schema/users.sql2
-rw-r--r--synapse/storage/stream.py2
-rw-r--r--synapse/storage/transactions.py2
-rw-r--r--synapse/streams/__init__.py2
-rw-r--r--synapse/streams/config.py2
-rw-r--r--synapse/streams/events.py2
-rw-r--r--synapse/types.py2
-rw-r--r--synapse/util/__init__.py2
-rw-r--r--synapse/util/async.py2
-rw-r--r--synapse/util/distributor.py19
-rw-r--r--synapse/util/jsonobject.py2
-rw-r--r--synapse/util/lockutils.py2
-rw-r--r--synapse/util/logutils.py2
-rw-r--r--synapse/util/stringutils.py2
-rw-r--r--tests/__init__.py2
-rw-r--r--tests/events/__init__.py2
-rw-r--r--tests/events/test_events.py2
-rw-r--r--tests/federation/test_federation.py2
-rw-r--r--tests/federation/test_pdu_codec.py2
-rw-r--r--tests/handlers/test_directory.py13
-rw-r--r--tests/handlers/test_federation.py2
-rw-r--r--tests/handlers/test_presence.py134
-rw-r--r--tests/handlers/test_presencelike.py53
-rw-r--r--tests/handlers/test_profile.py2
-rw-r--r--tests/handlers/test_room.py2
-rw-r--r--tests/handlers/test_typing.py2
-rw-r--r--tests/rest/__init__.py2
-rw-r--r--tests/rest/test_events.py2
-rw-r--r--tests/rest/test_presence.py50
-rw-r--r--tests/rest/test_profile.py2
-rw-r--r--tests/rest/test_rooms.py2
-rw-r--r--tests/rest/utils.py2
-rw-r--r--tests/storage/test_base.py2
-rw-r--r--tests/test_distributor.py26
-rw-r--r--tests/test_state.py2
-rw-r--r--tests/test_types.py2
-rw-r--r--tests/util/__init__.py2
-rw-r--r--tests/util/test_lock.py2
-rw-r--r--tests/utils.py2
-rw-r--r--webclient/app-controller.js2
-rw-r--r--webclient/app-directive.js2
-rw-r--r--webclient/app-filter.js19
-rw-r--r--webclient/app.js2
-rw-r--r--webclient/components/fileInput/file-input-directive.js2
-rw-r--r--webclient/components/fileUpload/file-upload-service.js2
-rw-r--r--webclient/components/matrix/event-handler-service.js28
-rw-r--r--webclient/components/matrix/event-stream-service.js2
-rw-r--r--webclient/components/matrix/matrix-call.js2
-rw-r--r--webclient/components/matrix/matrix-phone-service.js2
-rw-r--r--webclient/components/matrix/matrix-service.js61
-rw-r--r--webclient/components/matrix/presence-service.js2
-rw-r--r--webclient/components/utilities/utilities-service.js2
-rw-r--r--webclient/home/home-controller.js2
-rw-r--r--webclient/login/login-controller.js2
-rw-r--r--webclient/login/register-controller.js2
-rw-r--r--webclient/recents/recents-controller.js2
-rw-r--r--webclient/recents/recents-filter.js2
-rw-r--r--webclient/recents/recents.html4
-rw-r--r--webclient/room/room-controller.js128
-rw-r--r--webclient/room/room-directive.js2
-rw-r--r--webclient/room/room.html7
-rw-r--r--webclient/settings/settings-controller.js2
-rw-r--r--webclient/settings/settings.html15
-rw-r--r--webclient/user/user-controller.js2
168 files changed, 1898 insertions, 789 deletions
diff --git a/UPGRADE.rst b/UPGRADE.rst
index 99d58ab64f..da2a7a0a21 100644
--- a/UPGRADE.rst
+++ b/UPGRADE.rst
@@ -1,11 +1,6 @@
 Upgrading to v0.2.0
 ===================
 
-To upgrade the database schema, run::
-
-    ./database-prepare-for-0.2.0.sh "<database>.db"
-
-
 The home server now requires setting up of SSL config before it can run. To
 automatically generate default config use::
 
diff --git a/cmdclient/console.py b/cmdclient/console.py
index f6a7731eab..2e6b026762 100755
--- a/cmdclient/console.py
+++ b/cmdclient/console.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -88,6 +88,8 @@ class SynapseCmd(cmd.Cmd):
         return False
 
     def _domain(self):
+        if "user" not in self.config or not self.config["user"]:
+            return None
         return self.config["user"].split(":")[1]
 
     def do_config(self, line):
@@ -191,8 +193,10 @@ class SynapseCmd(cmd.Cmd):
                 p = getpass.getpass("Enter your password: ")
                 user = args["user_id"]
                 if self._is_on("complete_usernames") and not user.startswith("@"):
-                    user = "@" + user + ":" + self._domain()
-
+                    domain = self._domain()
+                    if domain:
+                        user = "@" + user + ":" + domain
+                
                 reactor.callFromThread(self._do_login, user, p)
                 #print " got %s " % p
         except Exception as e:
@@ -312,7 +316,7 @@ class SynapseCmd(cmd.Cmd):
         try:
             args = self._parse(line, ["roomname"], force_keys=True)
             path = "/join/%s" % urllib.quote(args["roomname"])
-            reactor.callFromThread(self._run_and_pprint, "PUT", path, {})
+            reactor.callFromThread(self._run_and_pprint, "POST", path, {})
         except Exception as e:
             print e
 
@@ -700,7 +704,7 @@ def main(server_url, identity_server_url, username, token, config_path):
 if __name__ == '__main__':
     parser = argparse.ArgumentParser("Starts a synapse client.")
     parser.add_argument(
-        "-s", "--server", dest="server", default="http://localhost:8080",
+        "-s", "--server", dest="server", default="http://localhost:8008",
         help="The URL of the home server to talk to.")
     parser.add_argument(
         "-i", "--identity-server", dest="identityserver", default="http://localhost:8090",
diff --git a/cmdclient/http.py b/cmdclient/http.py
index 9de6be9b72..869f782ec1 100644
--- a/cmdclient/http.py
+++ b/cmdclient/http.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/database-prepare-for-0.2.0.sh b/database-prepare-for-0.2.0.sh
deleted file mode 100755
index e90171010b..0000000000
--- a/database-prepare-for-0.2.0.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-# This is will prepare a synapse database for running with v0.2.0 of synapse. 
-
-set -e
-
-cp "$1" "$1.bak"
-
-sqlite3 "$1" < "synapse/storage/schema/im.sql" 
-sqlite3 "$1" <<< "PRAGMA user_version = 2;"
diff --git a/docs/client-server/specification.rst b/docs/client-server/OLD_specification.rst
index 2f6645ceb9..47fba5eeac 100644
--- a/docs/client-server/specification.rst
+++ b/docs/client-server/OLD_specification.rst
@@ -2,6 +2,20 @@
 Matrix Client-Server API
 ========================
 
+
+.. WARNING::
+  This specification is old. Please see /docs/specification.rst instead.
+
+
+
+
+
+
+
+
+
+
+
 The following specification outlines how a client can send and receive data from 
 a home server.
 
diff --git a/docs/client-server/swagger_matrix/api-docs-directory b/docs/client-server/swagger_matrix/api-docs-directory
index 98109a0fbc..ce12be8c96 100644
--- a/docs/client-server/swagger_matrix/api-docs-directory
+++ b/docs/client-server/swagger_matrix/api-docs-directory
@@ -1,7 +1,7 @@
 {
   "apiVersion": "1.0.0",
   "swaggerVersion": "1.2",
-  "basePath": "http://localhost:8080/_matrix/client/api/v1",
+  "basePath": "http://localhost:8008/_matrix/client/api/v1",
   "resourcePath": "/directory",
   "produces": [
     "application/json"
@@ -13,6 +13,7 @@
         {
           "method": "GET",
           "summary": "Get the room ID corresponding to this room alias.",
+          "notes": "Volatile: This API is likely to change.",
           "type": "DirectoryResponse",
           "nickname": "get_room_id_for_alias",
           "parameters": [
@@ -28,6 +29,7 @@
         {
           "method": "PUT",
           "summary": "Create a new mapping from room alias to room ID.",
+          "notes": "Volatile: This API is likely to change.",
           "type": "void",
           "nickname": "add_room_alias",
           "parameters": [
diff --git a/docs/client-server/swagger_matrix/api-docs-events b/docs/client-server/swagger_matrix/api-docs-events
index e5dd3a6113..1bdb9b034a 100644
--- a/docs/client-server/swagger_matrix/api-docs-events
+++ b/docs/client-server/swagger_matrix/api-docs-events
@@ -1,7 +1,7 @@
 {
   "apiVersion": "1.0.0",
   "swaggerVersion": "1.2",
-  "basePath": "http://localhost:8080/_matrix/client/api/v1",
+  "basePath": "http://localhost:8008/_matrix/client/api/v1",
   "resourcePath": "/events",
   "produces": [
     "application/json"
diff --git a/docs/client-server/swagger_matrix/api-docs-login b/docs/client-server/swagger_matrix/api-docs-login
index 8cc598b3c1..d6f8d84f29 100644
--- a/docs/client-server/swagger_matrix/api-docs-login
+++ b/docs/client-server/swagger_matrix/api-docs-login
@@ -8,7 +8,7 @@
           "nickname": "get_login_info", 
           "notes": "All login stages MUST be mentioned if there is >1 login type.", 
           "summary": "Get the login mechanism to use when logging in.", 
-          "type": "LoginInfo"
+          "type": "LoginFlows"
         }, 
         {
           "method": "POST", 
@@ -40,17 +40,31 @@
       "path": "/login"
     }
   ], 
-  "basePath": "http://localhost:8080/_matrix/client/api/v1", 
+  "basePath": "http://localhost:8008/_matrix/client/api/v1", 
   "consumes": [
     "application/json"
   ], 
   "models": {
+    "LoginFlows": {
+      "id": "LoginFlows",
+      "properties": {
+        "flows": {
+          "description": "A list of valid login flows.",
+          "type": "array",
+          "items": {
+            "$ref": "LoginInfo"
+          }
+        }
+      }
+    },
     "LoginInfo": {
       "id": "LoginInfo", 
       "properties": {
         "stages": {
           "description": "Multi-stage login only: An array of all the login types required to login.", 
-          "format": "string", 
+          "items": {
+            "$ref": "string"
+          }, 
           "type": "array"
         }, 
         "type": {
@@ -65,6 +79,10 @@
         "access_token": {
           "description": "The access token for this user's login if this is the final stage of the login process.", 
           "type": "string"
+        },
+        "user_id": {
+          "description": "The user's fully-qualified user ID.",
+          "type": "string"
         }, 
         "next": {
           "description": "Multi-stage login only: The next login type to submit.", 
diff --git a/docs/client-server/swagger_matrix/api-docs-presence b/docs/client-server/swagger_matrix/api-docs-presence
index d52ce2164a..6b22446024 100644
--- a/docs/client-server/swagger_matrix/api-docs-presence
+++ b/docs/client-server/swagger_matrix/api-docs-presence
@@ -1,7 +1,7 @@
 {
   "apiVersion": "1.0.0",
   "swaggerVersion": "1.2",
-  "basePath": "http://localhost:8080/_matrix/client/api/v1",
+  "basePath": "http://localhost:8008/_matrix/client/api/v1",
   "resourcePath": "/presence",
   "produces": [
     "application/json"
@@ -106,7 +106,7 @@
     "PresenceUpdate": {
       "id": "PresenceUpdate",
       "properties": {
-        "state": {
+        "presence": {
           "type": "string",
           "description": "Enum: The presence state.",
           "enum": [
@@ -128,10 +128,10 @@
     "Presence": {
       "id": "Presence",
       "properties": {
-        "mtime_age": {
+        "last_active_ago": {
           "type": "integer",
           "format": "int64",
-          "description": "The last time this user's presence state changed, in milliseconds."
+          "description": "The last time this user performed an action on their home server."
         },
         "user_id": {
           "type": "string",
diff --git a/docs/client-server/swagger_matrix/api-docs-profile b/docs/client-server/swagger_matrix/api-docs-profile
index 188259fa3d..d2fccaa67d 100644
--- a/docs/client-server/swagger_matrix/api-docs-profile
+++ b/docs/client-server/swagger_matrix/api-docs-profile
@@ -1,7 +1,7 @@
 {
   "apiVersion": "1.0.0",
   "swaggerVersion": "1.2",
-  "basePath": "http://localhost:8080/_matrix/client/api/v1",
+  "basePath": "http://localhost:8008/_matrix/client/api/v1",
   "resourcePath": "/profile",
   "produces": [
     "application/json"
diff --git a/docs/client-server/swagger_matrix/api-docs-registration b/docs/client-server/swagger_matrix/api-docs-registration
index 2048aec1d2..f4669ea2f0 100644
--- a/docs/client-server/swagger_matrix/api-docs-registration
+++ b/docs/client-server/swagger_matrix/api-docs-registration
@@ -37,7 +37,7 @@
       "path": "/register"
     }
   ], 
-  "basePath": "http://localhost:8080/_matrix/client/api/v1", 
+  "basePath": "http://localhost:8008/_matrix/client/api/v1", 
   "consumes": [
     "application/json"
   ], 
@@ -52,6 +52,10 @@
         "user_id": {
           "description": "The fully-qualified user ID.", 
           "type": "string"
+        },
+        "home_server": {
+          "description": "The name of the home server.",
+          "type": "string"
         }
       }
     }, 
diff --git a/docs/client-server/swagger_matrix/api-docs-rooms b/docs/client-server/swagger_matrix/api-docs-rooms
index 0a8bb3c2a5..0e1fa452a2 100644
--- a/docs/client-server/swagger_matrix/api-docs-rooms
+++ b/docs/client-server/swagger_matrix/api-docs-rooms
@@ -1,7 +1,7 @@
 {
   "apiVersion": "1.0.0",
   "swaggerVersion": "1.2",
-  "basePath": "http://localhost:8080/_matrix/client/api/v1", 
+  "basePath": "http://localhost:8008/_matrix/client/api/v1", 
   "resourcePath": "/rooms",
   "produces": [
     "application/json"
@@ -181,6 +181,59 @@
       ]
     },
     {
+      "path": "/rooms/{roomId}/state/m.room.name",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Set the name of this room.",
+          "notes": "Set the name of this room.",
+          "type": "void",
+          "nickname": "set_room_name",
+          "consumes": [
+            "application/json"
+          ],
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The name contents",
+              "required": true,
+              "type": "RoomName",
+              "paramType": "body"
+            },
+            {
+              "name": "roomId",
+              "description": "The room to set the name of.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get the room's name.",
+          "notes": "",
+          "type": "RoomName",
+          "nickname": "get_room_name",
+          "parameters": [
+            {
+              "name": "roomId",
+              "description": "The room to get the name of.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 404,
+              "message": "Name not found."
+            }
+          ]
+        }
+      ]
+    },
+    {
       "path": "/rooms/{roomId}/send/m.room.message.feedback",
       "operations": [
         {
@@ -267,6 +320,12 @@
               "required": true,
               "type": "string",
               "paramType": "path"
+            },
+            {
+              "name": "body",
+              "required": true,
+              "type": "JoinRequest",
+              "paramType": "body"
             }
           ]
         }  
@@ -291,6 +350,12 @@
               "required": true,
               "type": "string",
               "paramType": "path"
+            },
+            {
+              "name": "body",
+              "required": true,
+              "type": "LeaveRequest",
+              "paramType": "body"
             }
           ]
         }  
@@ -424,10 +489,10 @@
       "path": "/join/{roomAliasOrId}",
       "operations": [
         {
-          "method": "PUT",
+          "method": "POST",
           "summary": "Join a room via a room alias or room ID.",
           "notes": "Join a room via a room alias or room ID.",
-          "type": "RoomInfo",
+          "type": "JoinRoomInfo",
           "nickname": "join",
           "consumes": [
             "application/json"
@@ -574,7 +639,7 @@
         {
           "method": "GET",
           "summary": "Get a list of all the current state events for this room.",
-          "notes": "Get a list of all the current state events for this room.",
+          "notes": "NOT YET IMPLEMENTED.",
           "type": "array",
           "items": {
             "$ref": "Event"
@@ -598,7 +663,7 @@
         {
           "method": "GET",
           "summary": "Get all the current information for this room, including messages and state events.",
-          "notes": "Get all the current information for this room, including messages and state events.",
+          "notes": "NOT YET IMPLEMENTED.",
           "type": "InitialSyncRoomData",
           "nickname": "get_room_sync_data",
           "parameters": [
@@ -624,6 +689,15 @@
         }
       }
     },
+    "RoomName": {
+      "id": "RoomName",
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "The human-readable name for the room. Can contain spaces."
+        }
+      }
+    },
     "Message": {
       "id": "Message",
       "properties": {
@@ -640,6 +714,16 @@
     "Feedback": {
       "id": "Feedback",
       "properties": {
+        "target_event_id": {
+          "type": "string",
+          "description": "The event ID being acknowledged.",
+          "required": true
+        },
+        "type": {
+          "type": "string",
+          "description": "The type of feedback. Either 'delivered' or 'read'.",
+          "required": true
+        }
       }
     },
     "Member": {
@@ -652,7 +736,7 @@
             "invite",
             "join",
             "leave",
-            "knock"
+            "ban"
           ]
         }
       }
@@ -672,6 +756,16 @@
         }
       }
     },
+    "JoinRoomInfo": {
+      "id": "JoinRoomInfo",
+      "properties": {
+        "room_id": {
+          "type": "string",
+          "description": "The room ID joined, if joined via a room alias only.",
+          "required": true
+        }
+      }
+    },
     "RoomConfig": {
       "id": "RoomConfig",
       "properties": {
@@ -830,6 +924,14 @@
         }
       }
     },
+    "JoinRequest": {
+      "id": "JoinRequest",
+      "properties": {}
+    },
+    "LeaveRequest": {
+      "id": "LeaveRequest",
+      "properties": {}
+    },
     "BanRequest": {
       "id": "BanRequest",
       "properties": {
diff --git a/docs/server-server/security-threat-model.rst b/docs/server-server/security-threat-model.rst
deleted file mode 100644
index cf0430e43d..0000000000
--- a/docs/server-server/security-threat-model.rst
+++ /dev/null
@@ -1,141 +0,0 @@
-Overview
-========
-
-Scope
------
-
-This document considers threats specific to the server to server federation 
-synapse protocol.
-
-
-Attacker
---------
-
-It is assumed that the attacker can see and manipulate all network traffic 
-between any of the servers and may be in control of one or more homeservers 
-participating in the federation protocol.
-
-Threat Model
-============
-
-Denial of Service
------------------
-
-The attacker could attempt to prevent delivery of messages to or from the 
-victim in order to:
-
-    * Disrupt service or marketing campaign of a commercial competitor.
-    * Censor a discussion or censor a participant in a discussion.
-    * Perform general vandalism.
-
-Threat: Resource Exhaustion
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could cause the victims server to exhaust a particular resource 
-(e.g. open TCP connections, CPU, memory, disk storage)
-
-Threat: Unrecoverable Consistency Violations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could send messages which created an unrecoverable "split-brain"
-state in the cluster such that the victim's servers could no longer dervive a
-consistent view of the chatroom state.
-
-Threat: Bad History
-~~~~~~~~~~~~~~~~~~~
-
-An attacker could convince the victim to accept invalid messages which the 
-victim would then include in their view of the chatroom history. Other servers
-in the chatroom would reject the invalid messages and potentially reject the
-victims messages as well since they depended on the invalid messages.
-
-Threat: Block Network Traffic
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could try to firewall traffic between the victim's server and some
-or all of the other servers in the chatroom.
-
-Threat: High Volume of Messages
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could send large volumes of messages to a chatroom with the victim
-making the chatroom unusable.
-
-Threat: Banning users without necessary authorisation
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could attempt to ban a user from a chatroom with the necessary
-authorisation.
-
-Spoofing
---------
-
-An attacker could try to send a message claiming to be from the victim without 
-the victim having sent the message in order to:
-
-    * Impersonate the victim while performing illict activity.
-    * Obtain privileges of the victim.
-
-Threat: Altering Message Contents
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could try to alter the contents of an existing message from the 
-victim.
-
-Threat: Fake Message "origin" Field
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could try to send a new message purporting to be from the victim
-with a phony "origin" field.
-
-Spamming
---------
-
-The attacker could try to send a high volume of solicicted or unsolicted 
-messages to the victim in order to:
-    
-    * Find victims for scams.
-    * Market unwanted products.
-
-Threat: Unsoliticted Messages
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could try to send messages to victims who do not wish to receive 
-them.
-
-Threat: Abusive Messages
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could send abusive or threatening messages to the victim
-
-Spying
-------
-
-The attacker could try to access message contents or metadata for messages sent
-by the victim or to the victim that were not intended to reach the attacker in
-order to:
-
-    * Gain sensitive personal or commercial information.
-    * Impersonate the victim using credentials contained in the messages.
-      (e.g. password reset messages)
-    * Discover who the victim was talking to and when.
-
-Threat: Disclosure during Transmission
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could try to expose the message contents or metadata during 
-transmission between the servers.
-
-Threat: Disclosure to Servers Outside Chatroom
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could try to convince servers within a chatroom to send messages to
-a server it controls that was not authorised to be within the chatroom.
-
-Threat: Disclosure to Servers Within Chatroom
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An attacker could take control of a server within a chatroom to expose message
-contents or metadata for messages in that room.
-
-
diff --git a/docs/specification.rst b/docs/specification.rst
index fdd5f07917..239e51b4f3 100644
--- a/docs/specification.rst
+++ b/docs/specification.rst
@@ -30,48 +30,52 @@ ecosystem to communicate with one another.
 
 The principles that Matrix attempts to follow are:
 
- - Pragmatic Web-friendly APIs (i.e. JSON over REST)
- - Keep It Simple & Stupid
+- Pragmatic Web-friendly APIs (i.e. JSON over REST)
+- Keep It Simple & Stupid
 
-   + provide a simple architecture with minimal third-party dependencies.
+  + provide a simple architecture with minimal third-party dependencies.
 
- - Fully open:
+- Fully open:
 
-   + Fully open federation - anyone should be able to participate in the global Matrix network
-   + Fully open standard - publicly documented standard with no IP or patent licensing encumbrances
-   + Fully open source reference implementation - liberally-licensed example implementations with no
-     IP or patent licensing encumbrances
+  + Fully open federation - anyone should be able to participate in the global
+    Matrix network
+  + Fully open standard - publicly documented standard with no IP or patent
+    licensing encumbrances
+  + Fully open source reference implementation - liberally-licensed example
+    implementations with no IP or patent licensing encumbrances
 
- - Empowering the end-user
+- Empowering the end-user
 
-   + The user should be able to choose the server and clients they use
-   + The user should be control how private their communication is
-   + The user should know precisely where their data is stored
+  + The user should be able to choose the server and clients they use
+  + The user should be control how private their communication is
+  + The user should know precisely where their data is stored
 
- - Fully decentralised - no single points of control over conversations or the network as a whole
- - Learning from history to avoid repeating it
+- Fully decentralised - no single points of control over conversations or the
+  network as a whole
+- Learning from history to avoid repeating it
 
-   + Trying to take the best aspects of XMPP, SIP, IRC, SMTP, IMAP and NNTP whilst trying to avoid their failings
+  + Trying to take the best aspects of XMPP, SIP, IRC, SMTP, IMAP and NNTP
+    whilst trying to avoid their failings
 
 The functionality that Matrix provides includes:
 
- - Creation and management of fully distributed chat rooms with no
-   single points of control or failure
- - Eventually-consistent cryptographically secure synchronisation of room 
-   state across a global open network of federated servers and services
- - Sending and receiving extensible messages in a room with (optional)
-   end-to-end encryption
- - Extensible user management (inviting, joining, leaving, kicking, banning)
-   mediated by a power-level based user privilege system.
- - Extensible room state management (room naming, aliasing, topics, bans)
- - Extensible user profile management (avatars, displaynames, etc)
- - Managing user accounts (registration, login, logout)
- - Use of 3rd Party IDs (3PIDs) such as email addresses, phone numbers,
-   Facebook accounts to authenticate, identify and discover users on Matrix.
- - Trusted federation of Identity servers for:
-
-   + Publishing user public keys for PKI
-   + Mapping of 3PIDs to Matrix IDs
+- Creation and management of fully distributed chat rooms with no
+  single points of control or failure
+- Eventually-consistent cryptographically secure synchronisation of room
+  state across a global open network of federated servers and services
+- Sending and receiving extensible messages in a room with (optional)
+  end-to-end encryption
+- Extensible user management (inviting, joining, leaving, kicking, banning)
+  mediated by a power-level based user privilege system.
+- Extensible room state management (room naming, aliasing, topics, bans)
+- Extensible user profile management (avatars, displaynames, etc)
+- Managing user accounts (registration, login, logout)
+- Use of 3rd Party IDs (3PIDs) such as email addresses, phone numbers,
+  Facebook accounts to authenticate, identify and discover users on Matrix.
+- Trusted federation of Identity servers for:
+
+  + Publishing user public keys for PKI
+  + Mapping of 3PIDs to Matrix IDs
 
 The end goal of Matrix is to be a ubiquitous messaging layer for synchronising
 arbitrary data between sets of people, devices and services - be that for instant
@@ -428,9 +432,10 @@ event also has a ``creator`` key which contains the user ID of the room
 creator. It will also generate several other events in order to manage
 permissions in this room. This includes:
 
- - ``m.room.power_levels`` : Sets the authority of the room creator.
+ - ``m.room.power_levels`` : Sets the power levels of users.
  - ``m.room.join_rules`` : Whether the room is "invite-only" or not.
- - ``m.room.add_state_level``
+ - ``m.room.add_state_level``: The power level required in order to
+   add new state to the room (as opposed to updating exisiting state)
  - ``m.room.send_event_level`` : The power level required in order to
    send a message in this room.
  - ``m.room.ops_level`` : The power level required in order to kick or
@@ -463,6 +468,27 @@ Permissions
       Link through to respective sections where necessary. How does this tie in with permissions, e.g.
       give example of creating a read-only room.
 
+Permissions for rooms are done via the concept of power levels - to do any
+action in a room a user must have a suitable power level. 
+
+Power levels for users are defined in ``m.room.power_levels``, where both
+a default and specific users' power levels can be set. By default all users
+have a power level of 0.
+
+State events may contain a ``required_power_level`` key, which indicates the
+minimum power a user must have before they can update that state key. The only
+exception to this is when a user leaves a room.
+
+To perform certain actions there are additional power level requirements
+defined in the following state events:
+
+- ``m.room.send_event_level`` defines the minimum level for sending non-state 
+  events. Defaults to 5.
+- ``m.room.add_state_level`` defines the minimum level for adding new state,
+  rather than updating existing state. Defaults to 5.
+- ``m.room.ops_level`` defines the minimum levels to ban and kick other users.
+  This defaults to a kick and ban levels of 5 each.
+
 
 Joining rooms
 -------------
@@ -797,89 +823,88 @@ prefixed with ``m.``
     to force this state change directly will fail. See the `Rooms`_ section for how to 
     use the membership APIs.
 
-``m.room.config``
+``m.room.create``
   Summary:
-    The room config.
+    The first event in the room.
   Type: 
     State event
   JSON format:
-    TODO
+    ``{ "creator": "string"}``
   Example:
-    TODO
+    ``{ "creator": "@user:example.com" }``
   Description:
-    TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what
+    This is the first event in a room and cannot be changed. It acts as the 
+    root of all other events.
 
-``m.room.invite_join``
-  Summary:
-    TODO.
-  Type: 
-    State event
-  JSON format:
-    TODO
-  Example:
-    TODO
-  Description:
-    TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what
-    
 ``m.room.join_rules``
   Summary:
-    TODO.
+    Descripes how/if people are allowed to join.
   Type: 
     State event
   JSON format:
-    TODO
+    ``{ "join_rule": "enum [ public|knock|invite|private ]" }``
   Example:
-    TODO
+    ``{ "join_rule": "public" }``
   Description:
-    TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what
-    
+    TODO : Use docs/models/rooms.rst
+   
 ``m.room.power_levels``
   Summary:
-    TODO.
+    Defines the power levels of users in the room.
   Type: 
     State event
   JSON format:
-    TODO
+    ``{ "<user_id>": <int>, ..., "default": <int>}``
   Example:
-    TODO
+    ``{ "@user:example.com": 5, "@user2:example.com": 10, "default": 0 }`` 
   Description:
-    TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what
-    
+    If a user is in the list, then they have the associated power level. 
+    Otherwise they have the default level. If not ``default`` key is supplied,
+    it is assumed to be 0.
+
 ``m.room.add_state_level``
   Summary:
-    TODO.
+    Defines the minimum power level a user needs to add state.
   Type: 
     State event
   JSON format:
-    TODO
+    ``{ "level": <int> }``
   Example:
-    TODO
+    ``{ "level": 5 }``
   Description:
-    TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what
+    To add a new piece of state to the room a user must have the given power 
+    level. This does not apply to updating current state, which is goverened
+    by the ``required_power_level`` event key.
     
 ``m.room.send_event_level``
   Summary:
-    TODO.
+    Defines the minimum power level a user needs to send an event.
   Type: 
     State event
   JSON format:
-    TODO
+    ``{ "level": <int> }``
   Example:
-    TODO
+    ``{ "level": 0 }``
   Description:
-    TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what
-    
+    To send a new event into the room a user must have at least this power 
+    level. This allows ops to make the room read only by increasing this level,
+    or muting individual users by lowering their power level below this
+    threshold.
+
 ``m.room.ops_levels``
   Summary:
-    TODO.
+    Defines the minimum power levels that a user must have before they can 
+    kick and/or ban other users.
   Type: 
     State event
   JSON format:
-    TODO
+    ``{ "ban_level": <int>, "kick_level": <int> }``
   Example:
-    TODO
+    ``{ "ban_level": 5, "kick_level": 5 }``
   Description:
-    TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what
+    This defines who can ban and/or kick people in the room. Most of the time
+    ``ban_level`` will be greater than or equal to ``kick_level`` since 
+    banning is more severe than kicking.
 
 ``m.room.message``
   Summary:
@@ -1485,6 +1510,31 @@ Each transaction has:
  - An origin and destination server name.
  - A list of "previous IDs".
  - A list of PDUs and EDUs - the actual message payload that the Transaction carries.
+ 
+``origin``
+  Type: 
+    String
+  Description:
+    DNS name of homeserver making this transaction.
+    
+``ts``
+  Type: 
+    Integer
+  Description:
+    Timestamp in milliseconds on originating homeserver when this transaction 
+    started.
+    
+``previous_ids``
+  Type:
+    List of strings
+  Description:
+    List of transactions that were sent immediately prior to this transaction.
+    
+``pdus``
+  Type:
+    List of Objects.
+  Description:
+    List of updates contained in this transaction.
 
 ::
 
@@ -1526,8 +1576,98 @@ All PDUs have:
  - A list of other PDU IDs that have been seen recently on that context (regardless of which origin
    sent them)
 
-[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
-[origin,ref] pair like the prev_pdus are]]
+``context``
+  Type:
+    String
+  Description:
+    Event context identifier
+    
+``origin``
+  Type:
+    String
+  Description:
+    DNS name of homeserver that created this PDU.
+    
+``pdu_id``
+  Type:
+    String
+  Description:
+    Unique identifier for PDU within the context for the originating homeserver
+
+``ts``
+  Type:
+    Integer
+  Description:
+    Timestamp in milliseconds on originating homeserver when this PDU was created.
+
+``pdu_type``
+  Type:
+    String
+  Description:
+    PDU event type.
+
+``prev_pdus``
+  Type:
+    List of pairs of strings
+  Description:
+    The originating homeserver and PDU ids of the most recent PDUs the 
+    homeserver was aware of for this context when it made this PDU.
+
+``depth``
+  Type:
+    Integer
+  Description:
+    The maximum depth of the previous PDUs plus one.
+
+
+.. TODO paul
+  [[TODO(paul): Update this structure so that 'pdu_id' is a two-element
+  [origin,ref] pair like the prev_pdus are]]
+  
+
+For state updates:
+
+``is_state``
+  Type:
+    Boolean
+  Description:
+    True if this PDU is updating state.
+    
+``state_key``
+  Type:
+    String
+  Description:
+    Optional key identifying the updated state within the context.
+    
+``power_level``
+  Type:
+    Integer
+  Description:
+    The asserted power level of the user performing the update.
+    
+``min_update``
+  Type:
+    Integer
+  Description:
+    The required power level needed to replace this update.
+
+``prev_state_id``
+  Type:
+    String
+  Description:
+    PDU event type.
+    
+``prev_state_origin``
+  Type:
+    String
+  Description:
+    The PDU id of the update this replaces.
+    
+``user``
+  Type:
+    String
+  Description:
+    The user updating the state.
 
 ::
 
@@ -1568,12 +1708,13 @@ keys exist to support this:
   "prev_state_id":TODO
   "prev_state_origin":TODO}
 
-[[TODO(paul): At this point we should probably have a long description of how
-State management works, with descriptions of clobbering rules, power levels, etc
-etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
-so on. This part needs refining. And writing in its own document as the details
-relate to the server/system as a whole, not specifically to server-server
-federation.]]
+.. TODO paul
+  [[TODO(paul): At this point we should probably have a long description of how
+  State management works, with descriptions of clobbering rules, power levels, etc
+  etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
+  so on. This part needs refining. And writing in its own document as the details
+  relate to the server/system as a whole, not specifically to server-server
+  federation.]]
 
 EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
 "previous" IDs. The only mandatory fields for these are the type, origin and
@@ -1585,6 +1726,79 @@ destination home server names, and the actual nested content.
   "origin":"blue",
   "destination":"orange",
   "content":...}
+  
+  
+Protocol URLs
+=============
+.. WARNING::
+  This section may be misleading or inaccurate.
+
+All these URLs are namespaced within a prefix of::
+
+  /_matrix/federation/v1/...
+
+For active pushing of messages representing live activity "as it happens"::
+
+  PUT .../send/:transaction_id/
+    Body: JSON encoding of a single Transaction
+    Response: TODO
+
+The transaction_id path argument will override any ID given in the JSON body.
+The destination name will be set to that of the receiving server itself. Each
+embedded PDU in the transaction body will be processed.
+
+
+To fetch a particular PDU::
+
+  GET .../pdu/:origin/:pdu_id/
+    Response: JSON encoding of a single Transaction containing one PDU
+
+Retrieves a given PDU from the server. The response will contain a single new
+Transaction, inside which will be the requested PDU.
+  
+
+To fetch all the state of a given context::
+
+  GET .../state/:context/
+    Response: JSON encoding of a single Transaction containing multiple PDUs
+
+Retrieves a snapshot of the entire current state of the given context. The
+response will contain a single Transaction, inside which will be a list of
+PDUs that encode the state.
+
+To backfill events on a given context::
+
+  GET .../backfill/:context/
+    Query args: v, limit
+    Response: JSON encoding of a single Transaction containing multiple PDUs
+
+Retrieves a sliding-window history of previous PDUs that occurred on the
+given context. Starting from the PDU ID(s) given in the "v" argument, the
+PDUs that preceeded it are retrieved, up to a total number given by the
+"limit" argument. These are then returned in a new Transaction containing all
+off the PDUs.
+
+
+To stream events all the events::
+
+  GET .../pull/
+    Query args: origin, v
+    Response: JSON encoding of a single Transaction consisting of multiple PDUs
+
+Retrieves all of the transactions later than any version given by the "v"
+arguments.
+
+
+To make a query::
+
+  GET .../query/:query_type
+    Query args: as specified by the individual query types
+    Response: JSON encoding of a response object
+
+Performs a single query request on the receiving home server. The Query Type
+part of the path specifies the kind of query being made, and its query
+arguments have a meaning specific to that kind of query. The response is a
+JSON-encoded object whose meaning also depends on the kind of query.
 
 Backfilling
 -----------
@@ -1604,9 +1818,133 @@ SRV Records
 
 Security
 ========
+
 .. NOTE::
   This section is a work in progress.
 
+Threat Model
+------------
+
+Denial of Service
+~~~~~~~~~~~~~~~~~
+
+The attacker could attempt to prevent delivery of messages to or from the
+victim in order to:
+
+* Disrupt service or marketing campaign of a commercial competitor.
+* Censor a discussion or censor a participant in a discussion.
+* Perform general vandalism.
+
+Threat: Resource Exhaustion
++++++++++++++++++++++++++++
+
+An attacker could cause the victims server to exhaust a particular resource
+(e.g. open TCP connections, CPU, memory, disk storage)
+
+Threat: Unrecoverable Consistency Violations
+++++++++++++++++++++++++++++++++++++++++++++
+
+An attacker could send messages which created an unrecoverable "split-brain"
+state in the cluster such that the victim's servers could no longer dervive a
+consistent view of the chatroom state.
+
+Threat: Bad History
++++++++++++++++++++
+
+An attacker could convince the victim to accept invalid messages which the
+victim would then include in their view of the chatroom history. Other servers
+in the chatroom would reject the invalid messages and potentially reject the
+victims messages as well since they depended on the invalid messages.
+
+Threat: Block Network Traffic
++++++++++++++++++++++++++++++
+
+An attacker could try to firewall traffic between the victim's server and some
+or all of the other servers in the chatroom.
+
+Threat: High Volume of Messages
++++++++++++++++++++++++++++++++
+
+An attacker could send large volumes of messages to a chatroom with the victim
+making the chatroom unusable.
+
+Threat: Banning users without necessary authorisation
++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+An attacker could attempt to ban a user from a chatroom with the necessary
+authorisation.
+
+Spoofing
+~~~~~~~~
+
+An attacker could try to send a message claiming to be from the victim without
+the victim having sent the message in order to:
+
+* Impersonate the victim while performing illict activity.
+* Obtain privileges of the victim.
+
+Threat: Altering Message Contents
++++++++++++++++++++++++++++++++++
+
+An attacker could try to alter the contents of an existing message from the
+victim.
+
+Threat: Fake Message "origin" Field
++++++++++++++++++++++++++++++++++++
+
+An attacker could try to send a new message purporting to be from the victim
+with a phony "origin" field.
+
+Spamming
+~~~~~~~~
+
+The attacker could try to send a high volume of solicicted or unsolicted
+messages to the victim in order to:
+
+* Find victims for scams.
+* Market unwanted products.
+
+Threat: Unsoliticted Messages
++++++++++++++++++++++++++++++
+
+An attacker could try to send messages to victims who do not wish to receive
+them.
+
+Threat: Abusive Messages
+++++++++++++++++++++++++
+
+An attacker could send abusive or threatening messages to the victim
+
+Spying
+~~~~~~
+
+The attacker could try to access message contents or metadata for messages sent
+by the victim or to the victim that were not intended to reach the attacker in
+order to:
+
+* Gain sensitive personal or commercial information.
+* Impersonate the victim using credentials contained in the messages.
+  (e.g. password reset messages)
+* Discover who the victim was talking to and when.
+
+Threat: Disclosure during Transmission
+++++++++++++++++++++++++++++++++++++++
+
+An attacker could try to expose the message contents or metadata during
+transmission between the servers.
+
+Threat: Disclosure to Servers Outside Chatroom
+++++++++++++++++++++++++++++++++++++++++++++++
+
+An attacker could try to convince servers within a chatroom to send messages to
+a server it controls that was not authorised to be within the chatroom.
+
+Threat: Disclosure to Servers Within Chatroom
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An attacker could take control of a server within a chatroom to expose message
+contents or metadata for messages in that room.
+
 Rate limiting
 -------------
 Home servers SHOULD implement rate limiting to reduce the risk of being overloaded. If a
@@ -1660,57 +1998,121 @@ Glossary
 .. NOTE::
   This section is a work in progress.
 
-.. TODO
-  - domain specific words/acronyms with definitions
+Backfilling:
+  The process of synchronising historic state from one home server to another,
+  to backfill the event storage so that scrollback can be presented to the
+  client(s). Not to be confused with pagination.
+
+Context:
+  A single human-level entity of interest (currently, a chat room)
+
+EDU (Ephemeral Data Unit):
+  A message that relates directly to a given pair of home servers that are
+  exchanging it. EDUs are short-lived messages that related only to one single
+  pair of servers; they are not persisted for a long time and are not forwarded
+  on to other servers. Because of this, they have no internal ID nor previous
+  EDUs reference chain.
+
+Event:
+  A record of activity that records a single thing that happened on to a context
+  (currently, a chat room). These are the "chat messages" that Synapse makes
+  available.
+
+PDU (Persistent Data Unit):
+  A message that relates to a single context, irrespective of the server that
+  is communicating it. PDUs either encode a single Event, or a single State
+  change. A PDU is referred to by its PDU ID; the pair of its origin server
+  and local reference from that server.
+
+PDU ID:
+  The pair of PDU Origin and PDU Reference, that together globally uniquely
+  refers to a specific PDU.
+
+PDU Origin:
+  The name of the origin server that generated a given PDU. This may not be the
+  server from which it has been received, due to the way they are copied around
+  from server to server. The origin always records the original server that
+  created it.
+
+PDU Reference:
+  A local ID used to refer to a specific PDU from a given origin server. These
+  references are opaque at the protocol level, but may optionally have some
+  structured meaning within a given origin server or implementation.
+
+Presence:
+  The concept of whether a user is currently online, how available they declare
+  they are, and so on. See also: doc/model/presence
+
+Profile:
+  A set of metadata about a user, such as a display name, provided for the
+  benefit of other users. See also: doc/model/profiles
+
+Room ID:
+  An opaque string (of as-yet undecided format) that identifies a particular
+  room and used in PDUs referring to it.
+
+Room Alias:
+  A human-readable string of the form #name:some.domain that users can use as a
+  pointer to identify a room; a Directory Server will map this to its Room ID
+
+State:
+  A set of metadata maintained about a Context, which is replicated among the
+  servers in addition to the history of Events.
 
 User ID:
-  An opaque ID which identifies an end-user, which consists of some opaque 
-  localpart combined with the domain name of their home server. 
+  A string of the form @localpart:domain.name that identifies a user for
+  wire-protocol purposes. The localpart is meaningless outside of a particular
+  home server. This takes a human-readable form that end-users can use directly
+  if they so wish, avoiding the 3PIDs.
+
+Transaction:
+  A message which relates to the communication between a given pair of servers.
+  A transaction contains possibly-empty lists of PDUs and EDUs.
 
 
 .. Links through the external API docs are below
 .. =============================================
 
 .. |createRoom| replace:: ``/createRoom``
-.. _createRoom: /-rooms/create_room
+.. _createRoom: /docs/api/client-server/#!/-rooms/create_room
 
 .. |initialSync| replace:: ``/initialSync``
-.. _initialSync: /-events/initial_sync
+.. _initialSync: /docs/api/client-server/#!/-events/initial_sync
 
 .. |/rooms/<room_id>/initialSync| replace:: ``/rooms/<room_id>/initialSync``
-.. _/rooms/<room_id>/initialSync: /-rooms/get_room_sync_data
+.. _/rooms/<room_id>/initialSync: /docs/api/client-server/#!/-rooms/get_room_sync_data
 
 .. |login| replace:: ``/login``
-.. _login: /-login
+.. _login: /docs/api/client-server/#!/-login
 
 .. |/rooms/<room_id>/messages| replace:: ``/rooms/<room_id>/messages``
-.. _/rooms/<room_id>/messages: /-rooms/get_messages
+.. _/rooms/<room_id>/messages: /docs/api/client-server/#!/-rooms/get_messages
 
 .. |/rooms/<room_id>/members| replace:: ``/rooms/<room_id>/members``
-.. _/rooms/<room_id>/members: /-rooms/get_members
+.. _/rooms/<room_id>/members: /docs/api/client-server/#!/-rooms/get_members
 
 .. |/rooms/<room_id>/state| replace:: ``/rooms/<room_id>/state``
-.. _/rooms/<room_id>/state: /-rooms/get_state_events
+.. _/rooms/<room_id>/state: /docs/api/client-server/#!/-rooms/get_state_events
 
 .. |/rooms/<room_id>/send/<event_type>| replace:: ``/rooms/<room_id>/send/<event_type>``
-.. _/rooms/<room_id>/send/<event_type>: /-rooms/send_non_state_event
+.. _/rooms/<room_id>/send/<event_type>: /docs/api/client-server/#!/-rooms/send_non_state_event
 
 .. |/rooms/<room_id>/state/<event_type>/<state_key>| replace:: ``/rooms/<room_id>/state/<event_type>/<state_key>``
-.. _/rooms/<room_id>/state/<event_type>/<state_key>: /-rooms/send_state_event
+.. _/rooms/<room_id>/state/<event_type>/<state_key>: /docs/api/client-server/#!/-rooms/send_state_event
 
 .. |/rooms/<room_id>/invite| replace:: ``/rooms/<room_id>/invite``
-.. _/rooms/<room_id>/invite: /-rooms/invite
+.. _/rooms/<room_id>/invite: /docs/api/client-server/#!/-rooms/invite
 
 .. |/rooms/<room_id>/join| replace:: ``/rooms/<room_id>/join``
-.. _/rooms/<room_id>/join: /-rooms/join_room
+.. _/rooms/<room_id>/join: /docs/api/client-server/#!/-rooms/join_room
 
 .. |/rooms/<room_id>/leave| replace:: ``/rooms/<room_id>/leave``
-.. _/rooms/<room_id>/leave: /-rooms/leave
+.. _/rooms/<room_id>/leave: /docs/api/client-server/#!/-rooms/leave
 
 .. |/rooms/<room_id>/ban| replace:: ``/rooms/<room_id>/ban``
-.. _/rooms/<room_id>/ban: /-rooms/ban
+.. _/rooms/<room_id>/ban: /docs/api/client-server/#!/-rooms/ban
 
 .. |/join/<room_alias_or_id>| replace:: ``/join/<room_alias_or_id>``
-.. _/join/<room_alias_or_id>: /-rooms/join
+.. _/join/<room_alias_or_id>: /docs/api/client-server/#!/-rooms/join
 
-.. _`Event Stream`: /-events/get_event_stream
+.. _`Event Stream`: /docs/api/client-server/#!/-events/get_event_stream
diff --git a/experiments/cursesio.py b/experiments/cursesio.py
index 31fbda5504..95d87a1fda 100644
--- a/experiments/cursesio.py
+++ b/experiments/cursesio.py
@@ -1,4 +1,4 @@
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/experiments/test_messaging.py b/experiments/test_messaging.py
index 3ff7ab820f..fedf786cec 100644
--- a/experiments/test_messaging.py
+++ b/experiments/test_messaging.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/graph/graph.py b/graph/graph.py
index ac06d979e1..b2acadcf5e 100644
--- a/graph/graph.py
+++ b/graph/graph.py
@@ -1,4 +1,4 @@
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/scripts/copyrighter.pl b/scripts/copyrighter.pl
index e476c9cc85..7c03ef21fc 100755
--- a/scripts/copyrighter.pl
+++ b/scripts/copyrighter.pl
@@ -1,5 +1,5 @@
 #!/usr/bin/perl -pi
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
 # limitations under the License.
 
 $copyright = <<EOT;
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/scripts/gendoc.sh b/scripts/gendoc.sh
index 30ba1db629..3c849e52e1 100755
--- a/scripts/gendoc.sh
+++ b/scripts/gendoc.sh
@@ -9,4 +9,6 @@ perl -pi -e 's#<head>#<head><link rel="stylesheet" href="/site.css">#' $MATRIXDO
 
 perl -pi -e 's#<body>#<body><div id="header"><div id="headerContent">&nbsp;</div></div><div id="page"><div id="wrapper"><div style="text-align: center; padding: 40px;"><img src="/matrix.png" width="305" height="130" alt="[matrix]"/></div>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html
 
-perl -pi -e 's#</body>#</div></div><div id="footer"><div id="footerContent">&copy 2014 Matrix.org</div></div></body>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html
\ No newline at end of file
+perl -pi -e 's#</body>#</div></div><div id="footer"><div id="footerContent">&copy 2014 Matrix.org</div></div></body>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html
+
+scp -r $MATRIXDOTORG/docs matrix@ldc-prd-matrix-001:/sites/matrix-beta
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 0eec5c9354..a9470b4c9e 100755
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/__init__.py b/synapse/__init__.py
index b45cf47b56..564d789645 100644
--- a/synapse/__init__.py
+++ b/synapse/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/api/__init__.py b/synapse/api/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/synapse/api/__init__.py
+++ b/synapse/api/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 54ecbe5b3a..b4eda3df01 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/api/constants.py b/synapse/api/constants.py
index 668ffa07ca..fcef062fc9 100644
--- a/synapse/api/constants.py
+++ b/synapse/api/constants.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/api/errors.py b/synapse/api/errors.py
index ad79bc7ff9..84afe4fa37 100644
--- a/synapse/api/errors.py
+++ b/synapse/api/errors.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py
index 9502f5df8f..f95468fc65 100644
--- a/synapse/api/events/__init__.py
+++ b/synapse/api/events/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py
index 159728b2d2..a3b293e024 100644
--- a/synapse/api/events/factory.py
+++ b/synapse/api/events/factory.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/api/events/room.py b/synapse/api/events/room.py
index f6d3c59a9a..33f0f0cb99 100644
--- a/synapse/api/events/room.py
+++ b/synapse/api/events/room.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -103,8 +103,7 @@ class FeedbackEvent(SynapseEvent):
     def get_content_template(self):
         return {
             "type": u"string",
-            "target_event_id": u"string",
-            "msg_sender_id": u"string"
+            "target_event_id": u"string"
         }
 
 
diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py
index b9b6a9de1a..b25358090f 100644
--- a/synapse/api/ratelimiting.py
+++ b/synapse/api/ratelimiting.py
@@ -1,4 +1,4 @@
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/api/urls.py b/synapse/api/urls.py
index 3d0b5de965..6314f31f7a 100644
--- a/synapse/api/urls.py
+++ b/synapse/api/urls.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/app/__init__.py b/synapse/app/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/synapse/app/__init__.py
+++ b/synapse/app/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 8a7cd07fec..49cf928cc1 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -23,7 +23,8 @@ from twisted.enterprise import adbapi
 from twisted.web.resource import Resource
 from twisted.web.static import File
 from twisted.web.server import Site
-from synapse.http.server import JsonResource, RootRedirect, ContentRepoResource
+from synapse.http.server import JsonResource, RootRedirect
+from synapse.http.content_repository import ContentRepoResource
 from synapse.http.client import TwistedHttpClient
 from synapse.api.urls import (
     CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
@@ -74,7 +75,9 @@ class SynapseHomeServer(HomeServer):
         return File("webclient")  # TODO configurable?
 
     def build_resource_for_content_repo(self):
-        return ContentRepoResource(self, self.upload_dir, self.auth)
+        return ContentRepoResource(
+            self, self.upload_dir, self.auth, self.content_addr
+        )
 
     def build_db_pool(self):
         """ Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
@@ -90,20 +93,28 @@ class SynapseHomeServer(HomeServer):
             if row and row[0]:
                 user_version = row[0]
 
-                if user_version < SCHEMA_VERSION:
-                    # TODO(paul): add some kind of intelligent fixup here
-                    raise ValueError("Cannot use this database as the " +
-                        "schema version (%d) does not match (%d)" %
-                        (user_version, SCHEMA_VERSION)
+                if user_version > SCHEMA_VERSION:
+                    raise ValueError("Cannot use this database as it is too " +
+                        "new for the server to understand"
+                    )
+                elif user_version < SCHEMA_VERSION:
+                    logging.info("Upgrading database from version %d",
+                        user_version
                     )
 
+                    # Run every version since after the current version.
+                    for v in range(user_version + 1, SCHEMA_VERSION + 1):
+                        sql_script = read_schema("delta/v%d" % (v))
+                        c.executescript(sql_script)
+
+                    db_conn.commit()
+
             else:
                 for sql_loc in SCHEMAS:
                     sql_script = read_schema(sql_loc)
 
                     c.executescript(sql_script)
-                    db_conn.commit()
-
+                db_conn.commit()
                 c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
 
             c.close()
@@ -248,6 +259,7 @@ def setup():
         db_name=config.database_path,
         tls_context_factory=tls_context_factory,
         config=config,
+        content_addr=config.content_addr,
     )
 
     hs.register_servlets()
diff --git a/synapse/config/__init__.py b/synapse/config/__init__.py
index fe8a073cd3..f9811bfa04 100644
--- a/synapse/config/__init__.py
+++ b/synapse/config/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/config/_base.py b/synapse/config/_base.py
index 4b445806da..7dc68230bd 100644
--- a/synapse/config/_base.py
+++ b/synapse/config/_base.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/config/database.py b/synapse/config/database.py
index edf2361914..460445f15d 100644
--- a/synapse/config/database.py
+++ b/synapse/config/database.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py
index a9aa4c735c..76e2cdeddd 100644
--- a/synapse/config/homeserver.py
+++ b/synapse/config/homeserver.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,9 +18,10 @@ from .server import ServerConfig
 from .logger import LoggingConfig
 from .database import DatabaseConfig
 from .ratelimiting import RatelimitConfig
+from .repository import ContentRepositoryConfig
 
 class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
-                       RatelimitConfig):
+                       RatelimitConfig, ContentRepositoryConfig):
     pass
 
 if __name__=='__main__':
diff --git a/synapse/config/logger.py b/synapse/config/logger.py
index 8db6621ae8..56cd095433 100644
--- a/synapse/config/logger.py
+++ b/synapse/config/logger.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py
index ee572e1fbf..f126782b8d 100644
--- a/synapse/config/ratelimiting.py
+++ b/synapse/config/ratelimiting.py
@@ -1,4 +1,4 @@
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/config/repository.py b/synapse/config/repository.py
new file mode 100644
index 0000000000..407c8d6c24
--- /dev/null
+++ b/synapse/config/repository.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 ._base import Config
+import os
+
+class ContentRepositoryConfig(Config):
+    def __init__(self, args):
+        super(ContentRepositoryConfig, self).__init__(args)
+        self.max_upload_size = self.parse_size(args.max_upload_size)
+
+    def parse_size(self, string):
+        sizes = {"K": 1024, "M": 1024 * 1024}
+        size = 1
+        suffix = string[-1]
+        if suffix in sizes:
+            string = string[:-1]
+            size = sizes[suffix]
+        return int(string) * size
+
+    @classmethod
+    def add_arguments(cls, parser):
+        super(ContentRepositoryConfig, cls).add_arguments(parser)
+        db_group = parser.add_argument_group("content_repository")
+        db_group.add_argument(
+            "--max-upload-size", default="1M"
+        )
diff --git a/synapse/config/server.py b/synapse/config/server.py
index 36143e3c9c..516e4cf882 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -32,6 +32,14 @@ class ServerConfig(Config):
         self.webclient = True
         self.manhole = args.manhole
 
+        if not args.content_addr:
+            host = args.server_name
+            if ':' not in host:
+                host  = "%s:%d" % (host, args.bind_port)
+            args.content_addr = "https://%s" % (host,)
+
+        self.content_addr = args.content_addr
+
     @classmethod
     def add_arguments(cls, parser):
         super(ServerConfig, cls).add_arguments(parser)
@@ -50,13 +58,16 @@ class ServerConfig(Config):
                                   help="Local interface to listen on")
         server_group.add_argument("-D", "--daemonize", action='store_true',
                                   help="Daemonize the home server")
-        server_group.add_argument('--pid-file', default="hs.pid",
+        server_group.add_argument('--pid-file', default="homeserver.pid",
                                   help="When running as a daemon, the file to"
                                   " store the pid in")
         server_group.add_argument("--manhole", metavar="PORT", dest="manhole",
                                   type=int,
                                   help="Turn on the twisted telnet manhole"
                                   " service on the given port.")
+        server_group.add_argument("--content-addr", default=None,
+                                  help="The host and scheme to use for the "
+                                  "content repository")
 
     def read_signing_key(self, signing_key_path):
         signing_key_base64 = self.read_file(signing_key_path, "signing_key")
@@ -77,3 +88,4 @@ class ServerConfig(Config):
             with open(args.signing_key_path, "w") as signing_key_file:
                 key = nacl.signing.SigningKey.generate()
                 signing_key_file.write(encode_base64(key.encode()))
+
diff --git a/synapse/config/tls.py b/synapse/config/tls.py
index 16f6f3aba6..72d5518a89 100644
--- a/synapse/config/tls.py
+++ b/synapse/config/tls.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/crypto/__init__.py b/synapse/crypto/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/synapse/crypto/__init__.py
+++ b/synapse/crypto/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/crypto/context_factory.py b/synapse/crypto/context_factory.py
index 344f2dd218..f86bd19255 100644
--- a/synapse/crypto/context_factory.py
+++ b/synapse/crypto/context_factory.py
@@ -1,4 +1,4 @@
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/crypto/keyclient.py b/synapse/crypto/keyclient.py
index e615866b68..c11df5c529 100644
--- a/synapse/crypto/keyclient.py
+++ b/synapse/crypto/keyclient.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/crypto/keyserver.py b/synapse/crypto/keyserver.py
index 3d80a0e660..a23484dbae 100644
--- a/synapse/crypto/keyserver.py
+++ b/synapse/crypto/keyserver.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/crypto/resource/__init__.py b/synapse/crypto/resource/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/synapse/crypto/resource/__init__.py
+++ b/synapse/crypto/resource/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/crypto/resource/key.py b/synapse/crypto/resource/key.py
index 6aecd2b95f..48d14b9f4a 100644
--- a/synapse/crypto/resource/key.py
+++ b/synapse/crypto/resource/key.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/federation/__init__.py b/synapse/federation/__init__.py
index b15e7cf941..1351b68fd6 100644
--- a/synapse/federation/__init__.py
+++ b/synapse/federation/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/federation/pdu_codec.py b/synapse/federation/pdu_codec.py
index adc166c564..cef61108dd 100644
--- a/synapse/federation/pdu_codec.py
+++ b/synapse/federation/pdu_codec.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/federation/persistence.py b/synapse/federation/persistence.py
index 4cf72b2e42..de36a80e41 100644
--- a/synapse/federation/persistence.py
+++ b/synapse/federation/persistence.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py
index cadf574b3b..e12510017f 100644
--- a/synapse/federation/replication.py
+++ b/synapse/federation/replication.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -628,7 +628,6 @@ class _TransactionQueue(object):
 
             for deferred in deferreds:
                 deferred.errback(e)
-                yield deferred
 
         finally:
             # We want to be *very* sure we delete this after we stop processing
diff --git a/synapse/federation/transport.py b/synapse/federation/transport.py
index 50c3df4a5d..6e62ae7c74 100644
--- a/synapse/federation/transport.py
+++ b/synapse/federation/transport.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/federation/units.py b/synapse/federation/units.py
index b468f70546..9740431279 100644
--- a/synapse/federation/units.py
+++ b/synapse/federation/units.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py
index b2208b26c3..5308e2c8e1 100644
--- a/synapse/handlers/__init__.py
+++ b/synapse/handlers/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py
index 57429ad2ef..9989fe8670 100644
--- a/synapse/handlers/_base.py
+++ b/synapse/handlers/_base.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py
index 7c89150d99..1b9e831fc0 100644
--- a/synapse/handlers/directory.py
+++ b/synapse/handlers/directory.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ from twisted.internet import defer
 from ._base import BaseHandler
 
 from synapse.api.errors import SynapseError
+from synapse.http.client import HttpClient
 
 import logging
 
@@ -36,7 +37,7 @@ class DirectoryHandler(BaseHandler):
         )
 
     @defer.inlineCallbacks
-    def create_association(self, room_alias, room_id, servers):
+    def create_association(self, room_alias, room_id, servers=None):
         # TODO(erikj): Do auth.
 
         if not room_alias.is_mine:
@@ -47,6 +48,12 @@ class DirectoryHandler(BaseHandler):
 
         # TODO(erikj): Check if there is a current association.
 
+        if not servers:
+            servers = yield self.store.get_joined_hosts_for_room(room_id)
+
+        if not servers:
+            raise SynapseError(400, "Failed to get server list")
+
         yield self.store.create_room_alias_association(
             room_alias,
             room_id,
@@ -68,7 +75,10 @@ class DirectoryHandler(BaseHandler):
             result = yield self.federation.make_query(
                 destination=room_alias.domain,
                 query_type="directory",
-                args={"room_alias": room_alias.to_string()},
+                args={
+                    "room_alias": room_alias.to_string(),
+                    HttpClient.RETRY_DNS_LOOKUP_FAILURES: False
+                }
             )
 
             if result and "room_id" in result and "servers" in result:
@@ -79,6 +89,9 @@ class DirectoryHandler(BaseHandler):
             defer.returnValue({})
             return
 
+        extra_servers = yield self.store.get_joined_hosts_for_room(room_id)
+        servers = list(set(extra_servers) | set(servers))
+
         defer.returnValue({
             "room_id": room_id,
             "servers": servers,
diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py
index 980a169b25..fd24a11fb8 100644
--- a/synapse/handlers/events.py
+++ b/synapse/handlers/events.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -126,5 +126,7 @@ class EventHandler(BaseHandler):
             defer.returnValue(None)
             return
 
-        yield self.auth.check(event, raises=True)
+        if hasattr(event, "room_id"):
+            yield self.auth.check_joined_room(event.room_id, user.to_string())
+
         defer.returnValue(event)
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index eac110419c..84059d8033 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py
index 0220fa0604..6ee7ce5a2d 100644
--- a/synapse/handlers/login.py
+++ b/synapse/handlers/login.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index c9e3c4e451..dad2bbd1a4 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -142,7 +142,12 @@ class MessageHandler(BaseRoomHandler):
             SynapseError if something went wrong.
         """
 
-        snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
+        snapshot = yield self.store.snapshot_room(
+            event.room_id,
+            event.user_id,
+            state_type=event.type,
+            state_key=event.state_key,
+        )
 
         yield self.auth.check(event, snapshot, raises=True)
 
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index 9bfceda88a..c79bb6ff76 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -155,19 +155,18 @@ class PresenceHandler(BaseHandler):
         if observer_user == observed_user:
             defer.returnValue(True)
 
-        allowed_by_subscription = yield self.store.is_presence_visible(
+        if (yield self.store.user_rooms_intersect(
+            [u.to_string() for u in observer_user, observed_user]
+        )):
+            defer.returnValue(True)
+
+        if (yield self.store.is_presence_visible(
             observed_localpart=observed_user.localpart,
             observer_userid=observer_user.to_string(),
-        )
-
-        if allowed_by_subscription:
+        )):
             defer.returnValue(True)
 
-        share_room = yield self.store.do_users_share_a_room(
-            [observer_user, observed_user]
-        )
-
-        defer.returnValue(share_room)
+        defer.returnValue(False)
 
     @defer.inlineCallbacks
     def get_state(self, target_user, auth_user):
@@ -181,7 +180,7 @@ class PresenceHandler(BaseHandler):
             state = yield self.store.get_presence_state(target_user.localpart)
             if "mtime" in state:
                 del state["mtime"]
-            state["presence"] = state["state"]
+            state["presence"] = state.pop("state")
 
             if target_user in self._user_cachemap:
                 state["last_active"] = (
@@ -208,21 +207,17 @@ class PresenceHandler(BaseHandler):
             raise SynapseError(400, "User is not hosted on this Home Server")
 
         if target_user != auth_user:
-            raise AuthError(400, "Cannot set another user's displayname")
+            raise AuthError(400, "Cannot set another user's presence")
 
         if "status_msg" not in state:
             state["status_msg"] = None
 
         for k in state.keys():
-            if k not in ("presence", "state", "status_msg"):
+            if k not in ("presence", "status_msg"):
                 raise SynapseError(
                     400, "Unexpected presence state key '%s'" % (k,)
                 )
 
-        # Handle legacy "state" key for now
-        if "state" in state:
-            state["presence"] = state.pop("state")
-
         if state["presence"] not in self.STATE_LEVELS:
             raise SynapseError(400, "'%s' is not a valid presence state" %
                 state["presence"]
@@ -601,7 +596,7 @@ class PresenceHandler(BaseHandler):
         if state is None:
             state = yield self.store.get_presence_state(user.localpart)
             del state["mtime"]
-            state["presence"] = state["state"]
+            state["presence"] = state.pop("state")
 
             if user in self._user_cachemap:
                 state["last_active"] = (
@@ -622,8 +617,6 @@ class PresenceHandler(BaseHandler):
             "user_id": user.to_string(),
         }
         user_state.update(**state)
-        if "state" in user_state and "presence" not in user_state:
-            user_state["presence"] = user_state["state"]
 
         yield self.federation.send_edu(
             destination=destination,
@@ -655,21 +648,12 @@ class PresenceHandler(BaseHandler):
             state = dict(push)
             del state["user_id"]
 
-            if "presence" in state:
-                # all is OK
-                pass
-            elif "state" in state:
-                # Legacy handling
-                state["presence"] = state["state"]
-            else:
+            if "presence" not in state:
                 logger.warning("Received a presence 'push' EDU from %s without"
-                    + " either a 'presence' or 'state' key", origin
+                    + " a 'presence' key", origin
                 )
                 continue
 
-            if "state" in state:
-                del state["state"]
-
             if "last_active_ago" in state:
                 state["last_active"] = int(
                     self.clock.time_msec() - state.pop("last_active_ago")
@@ -773,15 +757,52 @@ class PresenceEventSource(object):
         self.hs = hs
         self.clock = hs.get_clock()
 
+    @defer.inlineCallbacks
+    def is_visible(self, observer_user, observed_user):
+        if observer_user == observed_user:
+            defer.returnValue(True)
+
+        presence = self.hs.get_handlers().presence_handler
+
+        if (yield presence.store.user_rooms_intersect(
+            [u.to_string() for u in observer_user, observed_user]
+        )):
+            defer.returnValue(True)
+
+        if observed_user.is_mine:
+            pushmap = presence._local_pushmap
+
+            defer.returnValue(
+                observed_user.localpart in pushmap and
+                observer_user in pushmap[observed_user.localpart]
+            )
+        else:
+            recvmap = presence._remote_recvmap
+
+            defer.returnValue(
+                observed_user in recvmap and
+                observer_user in recvmap[observed_user]
+            )
+
+    @defer.inlineCallbacks
     def get_new_events_for_user(self, user, from_key, limit):
         from_key = int(from_key)
 
+        observer_user = user
+
         presence = self.hs.get_handlers().presence_handler
         cachemap = presence._user_cachemap
 
-        # TODO(paul): limit, and filter by visibility
-        updates = [(k, cachemap[k]) for k in cachemap
-                   if from_key < cachemap[k].serial]
+        updates = []
+        # TODO(paul): use a DeferredList ? How to limit concurrency.
+        for observed_user in cachemap.keys():
+            if not (from_key < cachemap[observed_user].serial):
+                continue
+
+            if (yield self.is_visible(observer_user, observed_user)):
+                updates.append((observed_user, cachemap[observed_user]))
+
+        # TODO(paul): limit
 
         if updates:
             clock = self.clock
@@ -789,20 +810,23 @@ class PresenceEventSource(object):
             latest_serial = max([x[1].serial for x in updates])
             data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
 
-            return ((data, latest_serial))
+            defer.returnValue((data, latest_serial))
         else:
-            return (([], presence._user_cachemap_latest_serial))
+            defer.returnValue(([], presence._user_cachemap_latest_serial))
 
     def get_current_key(self):
         presence = self.hs.get_handlers().presence_handler
         return presence._user_cachemap_latest_serial
 
+    @defer.inlineCallbacks
     def get_pagination_rows(self, user, pagination_config, key):
         # TODO (erikj): Does this make sense? Ordering?
 
         from_token = pagination_config.from_token
         to_token = pagination_config.to_token
 
+        observer_user = user
+
         from_key = int(from_token.presence_key)
 
         if to_token:
@@ -813,7 +837,17 @@ class PresenceEventSource(object):
         presence = self.hs.get_handlers().presence_handler
         cachemap = presence._user_cachemap
 
-        # TODO(paul): limit, and filter by visibility
+        updates = []
+        # TODO(paul): use a DeferredList ? How to limit concurrency.
+        for observed_user in cachemap.keys():
+            if not (to_key < cachemap[observed_user].serial < from_key):
+                continue
+
+            if (yield self.is_visible(observer_user, observed_user)):
+                updates.append((observed_user, cachemap[observed_user]))
+
+        # TODO(paul): limit
+
         updates = [(k, cachemap[k]) for k in cachemap
                    if to_key < cachemap[k].serial < from_key]
 
@@ -831,13 +865,13 @@ class PresenceEventSource(object):
             next_token = next_token.copy_and_replace(
                 "presence_key", earliest_serial
             )
-            return ((data, next_token))
+            defer.returnValue((data, next_token))
         else:
             if not to_token:
                 to_token = from_token.copy_and_replace(
                     "presence_key", 0
                 )
-            return (([], to_token))
+            defer.returnValue(([], to_token))
 
 
 class UserPresenceCache(object):
@@ -851,7 +885,6 @@ class UserPresenceCache(object):
 
     def update(self, state, serial):
         assert("mtime_age" not in state)
-        assert("state" not in state)
 
         self.state.update(state)
         # Delete keys that are now 'None'
@@ -869,11 +902,6 @@ class UserPresenceCache(object):
     def get_state(self):
         # clone it so caller can't break our cache
         state = dict(self.state)
-
-        # Legacy handling
-        if "presence" in state:
-            state["state"] = state["presence"]
-
         return state
 
     def make_event(self, user, clock):
diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py
index 6799132054..023d8c0cf2 100644
--- a/synapse/handlers/profile.py
+++ b/synapse/handlers/profile.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py
index d56d2663d7..bee052274f 100644
--- a/synapse/handlers/register.py
+++ b/synapse/handlers/register.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 58afced8f5..64956c320c 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py
index 3268427ecd..0ca4e5c31e 100644
--- a/synapse/handlers/typing.py
+++ b/synapse/handlers/typing.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/http/__init__.py b/synapse/http/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/synapse/http/__init__.py
+++ b/synapse/http/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/http/client.py b/synapse/http/client.py
index 5a5e40a26e..ebf1aa47c4 100644
--- a/synapse/http/client.py
+++ b/synapse/http/client.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
 
 
 from twisted.internet import defer, reactor
+from twisted.internet.error import DNSLookupError
 from twisted.web.client import _AgentBase, _URI, readBody, FileBodyProducer
 from twisted.web.http_headers import Headers
 
@@ -23,7 +24,7 @@ from synapse.util.async import sleep
 
 from syutil.jsonutil import encode_canonical_json
 
-from synapse.api.errors import CodeMessageException
+from synapse.api.errors import CodeMessageException, SynapseError
 
 from StringIO import StringIO
 
@@ -45,6 +46,7 @@ _destination_mappings = {
 class HttpClient(object):
     """ Interface for talking json over http
     """
+    RETRY_DNS_LOOKUP_FAILURES = "__retry_dns"
 
     def put_json(self, destination, path, data):
         """ Sends the specifed json data using PUT
@@ -144,13 +146,23 @@ class TwistedHttpClient(HttpClient):
             destination = _destination_mappings[destination]
 
         logger.debug("get_json args: %s", args)
+
+        retry_on_dns_fail = True
+        if HttpClient.RETRY_DNS_LOOKUP_FAILURES in args:
+            # FIXME: This isn't ideal, but the interface exposed in get_json
+            # isn't comprehensive enough to give caller's any control over
+            # their connection mechanics.
+            retry_on_dns_fail = args.pop(HttpClient.RETRY_DNS_LOOKUP_FAILURES)
+
         query_bytes = urllib.urlencode(args, True)
+        logger.debug("Query bytes: %s Retry DNS: %s", args, retry_on_dns_fail)
 
         response = yield self._create_request(
             destination.encode("ascii"),
             "GET",
             path.encode("ascii"),
-            query_bytes=query_bytes
+            query_bytes=query_bytes,
+            retry_on_dns_fail=retry_on_dns_fail
         )
 
         body = yield readBody(response)
@@ -179,7 +191,8 @@ class TwistedHttpClient(HttpClient):
 
     @defer.inlineCallbacks
     def _create_request(self, destination, method, path_bytes, param_bytes=b"",
-                        query_bytes=b"", producer=None, headers_dict={}):
+                        query_bytes=b"", producer=None, headers_dict={},
+                        retry_on_dns_fail=True):
         """ Creates and sends a request to the given url
         """
         headers_dict[b"User-Agent"] = [b"Synapse"]
@@ -218,6 +231,11 @@ class TwistedHttpClient(HttpClient):
                 logger.debug("Got response to %s", method)
                 break
             except Exception as e:
+                if not retry_on_dns_fail and isinstance(e, DNSLookupError):
+                    logger.warn("DNS Lookup failed to %s with %s", destination,
+                                e)
+                    raise SynapseError(400, "Domain specified not found.")
+
                 logger.exception("Got error in _create_request")
                 _print_ex(e)
 
diff --git a/synapse/http/content_repository.py b/synapse/http/content_repository.py
new file mode 100644
index 0000000000..7dd4a859f8
--- /dev/null
+++ b/synapse/http/content_repository.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 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.
+
+from .server import respond_with_json_bytes
+
+from synapse.util.stringutils import random_string
+from synapse.api.errors import (
+    cs_exception, SynapseError, CodeMessageException, Codes, cs_error
+)
+
+from twisted.protocols.basic import FileSender
+from twisted.web import server, resource
+from twisted.internet import defer
+
+import base64
+import json
+import logging
+import os
+import re
+
+logger = logging.getLogger(__name__)
+
+
+class ContentRepoResource(resource.Resource):
+    """Provides file uploading and downloading.
+
+    Uploads are POSTed to wherever this Resource is linked to. This resource
+    returns a "content token" which can be used to GET this content again. The
+    token is typically a path, but it may not be. Tokens can expire, be one-time
+    uses, etc.
+
+    In this case, the token is a path to the file and contains 3 interesting
+    sections:
+        - User ID base64d (for namespacing content to each user)
+        - random 24 char string
+        - Content type base64d (so we can return it when clients GET it)
+
+    """
+    isLeaf = True
+
+    def __init__(self, hs, directory, auth, external_addr):
+        resource.Resource.__init__(self)
+        self.hs = hs
+        self.directory = directory
+        self.auth = auth
+        self.external_addr = external_addr.rstrip('/')
+        self.max_upload_size = hs.config.max_upload_size
+
+        if not os.path.isdir(self.directory):
+            os.mkdir(self.directory)
+            logger.info("ContentRepoResource : Created %s directory.",
+                        self.directory)
+
+    @defer.inlineCallbacks
+    def map_request_to_name(self, request):
+        # auth the user
+        auth_user = yield self.auth.get_user_by_req(request)
+
+        # namespace all file uploads on the user
+        prefix = base64.urlsafe_b64encode(
+            auth_user.to_string()
+        ).replace('=', '')
+
+        # use a random string for the main portion
+        main_part = random_string(24)
+
+        # suffix with a file extension if we can make one. This is nice to
+        # provide a hint to clients on the file information. We will also reuse
+        # this info to spit back the content type to the client.
+        suffix = ""
+        if request.requestHeaders.hasHeader("Content-Type"):
+            content_type = request.requestHeaders.getRawHeaders(
+                "Content-Type")[0]
+            suffix = "." + base64.urlsafe_b64encode(content_type)
+            if (content_type.split("/")[0].lower() in
+                    ["image", "video", "audio"]):
+                file_ext = content_type.split("/")[-1]
+                # be a little paranoid and only allow a-z
+                file_ext = re.sub("[^a-z]", "", file_ext)
+                suffix += "." + file_ext
+
+        file_name = prefix + main_part + suffix
+        file_path = os.path.join(self.directory, file_name)
+        logger.info("User %s is uploading a file to path %s",
+                    auth_user.to_string(),
+                    file_path)
+
+        # keep trying to make a non-clashing file, with a sensible max attempts
+        attempts = 0
+        while os.path.exists(file_path):
+            main_part = random_string(24)
+            file_name = prefix + main_part + suffix
+            file_path = os.path.join(self.directory, file_name)
+            attempts += 1
+            if attempts > 25:  # really? Really?
+                raise SynapseError(500, "Unable to create file.")
+
+        defer.returnValue(file_path)
+
+    def render_GET(self, request):
+        # no auth here on purpose, to allow anyone to view, even across home
+        # servers.
+
+        # TODO: A little crude here, we could do this better.
+        filename = request.path.split('/')[-1]
+        # be paranoid
+        filename = re.sub("[^0-9A-z.-_]", "", filename)
+
+        file_path = self.directory + "/" + filename
+
+        logger.debug("Searching for %s", file_path)
+
+        if os.path.isfile(file_path):
+            # filename has the content type
+            base64_contentype = filename.split(".")[1]
+            content_type = base64.urlsafe_b64decode(base64_contentype)
+            logger.info("Sending file %s", file_path)
+            f = open(file_path, 'rb')
+            request.setHeader('Content-Type', content_type)
+            d = FileSender().beginFileTransfer(f, request)
+
+            # after the file has been sent, clean up and finish the request
+            def cbFinished(ignored):
+                f.close()
+                request.finish()
+            d.addCallback(cbFinished)
+        else:
+            respond_with_json_bytes(
+                request,
+                404,
+                json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)),
+                send_cors=True)
+
+        return server.NOT_DONE_YET
+
+    def render_POST(self, request):
+        self._async_render(request)
+        return server.NOT_DONE_YET
+
+    def render_OPTIONS(self, request):
+        respond_with_json_bytes(request, 200, {}, send_cors=True)
+        return server.NOT_DONE_YET
+
+    @defer.inlineCallbacks
+    def _async_render(self, request):
+        try:
+            # TODO: The checks here are a bit late. The content will have
+            # already been uploaded to a tmp file at this point
+            content_length = request.getHeader("Content-Length")
+            if content_length is None:
+                raise SynapseError(
+                    msg="Request must specify a Content-Length", code=400
+                )
+            if int(content_length) > self.max_upload_size:
+                raise SynapseError(
+                    msg="Upload request body is too large",
+                    code=413,
+                )
+
+            fname = yield self.map_request_to_name(request)
+
+            # TODO I have a suspcious feeling this is just going to block
+            with open(fname, "wb") as f:
+                f.write(request.content.read())
+
+
+            # FIXME (erikj): These should use constants.
+            file_name = os.path.basename(fname)
+            # FIXME: we can't assume what the public mounted path of the repo is
+            # ...plus self-signed SSL won't work to remote clients anyway
+            # ...and we can't assume that it's SSL anyway, as we might want to
+            # server it via the non-SSL listener...
+            url = "%s/_matrix/content/%s" % (
+                self.external_addr, file_name
+            )
+
+            respond_with_json_bytes(request, 200,
+                                    json.dumps({"content_token": url}),
+                                    send_cors=True)
+
+        except CodeMessageException as e:
+            logger.exception(e)
+            respond_with_json_bytes(request, e.code,
+                                    json.dumps(cs_exception(e)))
+        except Exception as e:
+            logger.error("Failed to store file: %s" % e)
+            respond_with_json_bytes(
+                request,
+                500,
+                json.dumps({"error": "Internal server error"}),
+                send_cors=True)
+
+
+
diff --git a/synapse/http/endpoint.py b/synapse/http/endpoint.py
index 6c1fdcb853..7018ee3458 100644
--- a/synapse/http/endpoint.py
+++ b/synapse/http/endpoint.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 74c220e869..8d419c02dd 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,22 +18,16 @@ from syutil.jsonutil import (
     encode_canonical_json, encode_pretty_printed_json
 )
 from synapse.api.errors import (
-    cs_exception, SynapseError, CodeMessageException, Codes, cs_error
+    cs_exception, SynapseError, CodeMessageException
 )
-from synapse.util.stringutils import random_string
 
 from twisted.internet import defer, reactor
-from twisted.protocols.basic import FileSender
 from twisted.web import server, resource
 from twisted.web.server import NOT_DONE_YET
 from twisted.web.util import redirectTo
 
-import base64
 import collections
-import json
 import logging
-import os
-import re
 
 logger = logging.getLogger(__name__)
 
@@ -198,161 +192,6 @@ class RootRedirect(resource.Resource):
         return resource.Resource.getChild(self, name, request)
 
 
-class ContentRepoResource(resource.Resource):
-    """Provides file uploading and downloading.
-
-    Uploads are POSTed to wherever this Resource is linked to. This resource
-    returns a "content token" which can be used to GET this content again. The
-    token is typically a path, but it may not be. Tokens can expire, be one-time
-    uses, etc.
-
-    In this case, the token is a path to the file and contains 3 interesting
-    sections:
-        - User ID base64d (for namespacing content to each user)
-        - random 24 char string
-        - Content type base64d (so we can return it when clients GET it)
-
-    """
-    isLeaf = True
-
-    def __init__(self, hs, directory, auth):
-        resource.Resource.__init__(self)
-        self.hs = hs
-        self.directory = directory
-        self.auth = auth
-
-        if not os.path.isdir(self.directory):
-            os.mkdir(self.directory)
-            logger.info("ContentRepoResource : Created %s directory.",
-                        self.directory)
-
-    @defer.inlineCallbacks
-    def map_request_to_name(self, request):
-        # auth the user
-        auth_user = yield self.auth.get_user_by_req(request)
-
-        # namespace all file uploads on the user
-        prefix = base64.urlsafe_b64encode(
-            auth_user.to_string()
-        ).replace('=', '')
-
-        # use a random string for the main portion
-        main_part = random_string(24)
-
-        # suffix with a file extension if we can make one. This is nice to
-        # provide a hint to clients on the file information. We will also reuse
-        # this info to spit back the content type to the client.
-        suffix = ""
-        if request.requestHeaders.hasHeader("Content-Type"):
-            content_type = request.requestHeaders.getRawHeaders(
-                "Content-Type")[0]
-            suffix = "." + base64.urlsafe_b64encode(content_type)
-            if (content_type.split("/")[0].lower() in
-                    ["image", "video", "audio"]):
-                file_ext = content_type.split("/")[-1]
-                # be a little paranoid and only allow a-z
-                file_ext = re.sub("[^a-z]", "", file_ext)
-                suffix += "." + file_ext
-
-        file_name = prefix + main_part + suffix
-        file_path = os.path.join(self.directory, file_name)
-        logger.info("User %s is uploading a file to path %s",
-                    auth_user.to_string(),
-                    file_path)
-
-        # keep trying to make a non-clashing file, with a sensible max attempts
-        attempts = 0
-        while os.path.exists(file_path):
-            main_part = random_string(24)
-            file_name = prefix + main_part + suffix
-            file_path = os.path.join(self.directory, file_name)
-            attempts += 1
-            if attempts > 25:  # really? Really?
-                raise SynapseError(500, "Unable to create file.")
-
-        defer.returnValue(file_path)
-
-    def render_GET(self, request):
-        # no auth here on purpose, to allow anyone to view, even across home
-        # servers.
-
-        # TODO: A little crude here, we could do this better.
-        filename = request.path.split('/')[-1]
-        # be paranoid
-        filename = re.sub("[^0-9A-z.-_]", "", filename)
-
-        file_path = self.directory + "/" + filename
-
-        logger.debug("Searching for %s", file_path)
-
-        if os.path.isfile(file_path):
-            # filename has the content type
-            base64_contentype = filename.split(".")[1]
-            content_type = base64.urlsafe_b64decode(base64_contentype)
-            logger.info("Sending file %s", file_path)
-            f = open(file_path, 'rb')
-            request.setHeader('Content-Type', content_type)
-            d = FileSender().beginFileTransfer(f, request)
-
-            # after the file has been sent, clean up and finish the request
-            def cbFinished(ignored):
-                f.close()
-                request.finish()
-            d.addCallback(cbFinished)
-        else:
-            respond_with_json_bytes(
-                request,
-                404,
-                json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)),
-                send_cors=True)
-
-        return server.NOT_DONE_YET
-
-    def render_POST(self, request):
-        self._async_render(request)
-        return server.NOT_DONE_YET
-
-    def render_OPTIONS(self, request):
-        respond_with_json_bytes(request, 200, {}, send_cors=True)
-        return server.NOT_DONE_YET
-
-    @defer.inlineCallbacks
-    def _async_render(self, request):
-        try:
-            fname = yield self.map_request_to_name(request)
-
-            # TODO I have a suspcious feeling this is just going to block
-            with open(fname, "wb") as f:
-                f.write(request.content.read())
-
-
-            # FIXME (erikj): These should use constants.
-            file_name = os.path.basename(fname)
-            # FIXME: we can't assume what the public mounted path of the repo is
-            # ...plus self-signed SSL won't work to remote clients anyway
-            # ...and we can't assume that it's SSL anyway, as we might want to
-            # server it via the non-SSL listener...
-            url = "https://%s/_matrix/content/%s" % (
-                self.hs.domain_with_port, file_name
-            )
-
-            respond_with_json_bytes(request, 200,
-                                    json.dumps({"content_token": url}),
-                                    send_cors=True)
-
-        except CodeMessageException as e:
-            logger.exception(e)
-            respond_with_json_bytes(request, e.code,
-                                    json.dumps(cs_exception(e)))
-        except Exception as e:
-            logger.error("Failed to store file: %s" % e)
-            respond_with_json_bytes(
-                request,
-                500,
-                json.dumps({"error": "Internal server error"}),
-                send_cors=True)
-
-
 def respond_with_json_bytes(request, code, json_bytes, send_cors=False,
                             response_code_message=None):
     """Sends encoded JSON in response to the given request.
diff --git a/synapse/notifier.py b/synapse/notifier.py
index 3260aa744f..5b02c71d1e 100644
--- a/synapse/notifier.py
+++ b/synapse/notifier.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -167,7 +167,12 @@ class Notifier(object):
                 )
 
         def eb(failure):
-            logger.exception("Failed to notify listener", failure)
+            logger.error("Failed to notify listener",
+                exc_info=(
+                    failure.type,
+                    failure.value,
+                    failure.getTracebackObject())
+            )
 
         yield defer.DeferredList(
             [notify(l).addErrback(eb) for l in listeners]
diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py
index f33024e72a..ed785cfbd5 100644
--- a/synapse/rest/__init__.py
+++ b/synapse/rest/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/rest/base.py b/synapse/rest/base.py
index e855d293e5..2e8e3fa7d4 100644
--- a/synapse/rest/base.py
+++ b/synapse/rest/base.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/rest/directory.py b/synapse/rest/directory.py
index dc347652a0..18df7c8d8b 100644
--- a/synapse/rest/directory.py
+++ b/synapse/rest/directory.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
 
 from twisted.internet import defer
 
+from synapse.api.errors import SynapseError, Codes
 from base import RestServlet, client_path_pattern
 
 import json
@@ -44,8 +45,10 @@ class ClientDirectoryServer(RestServlet):
 
     @defer.inlineCallbacks
     def on_PUT(self, request, room_alias):
-        # TODO(erikj): Exceptions
-        content = json.loads(request.content.read())
+        content = _parse_json(request)
+        if not "room_id" in content:
+            raise SynapseError(400, "Missing room_id key",
+                               errcode=Codes.BAD_JSON)
 
         logger.debug("Got content: %s", content)
 
@@ -54,7 +57,7 @@ class ClientDirectoryServer(RestServlet):
         logger.debug("Got room name: %s", room_alias.to_string())
 
         room_id = content["room_id"]
-        servers = content["servers"]
+        servers = content["servers"] if "servers" in content else None
 
         logger.debug("Got room_id: %s", room_id)
         logger.debug("Got servers: %s", servers)
@@ -68,7 +71,20 @@ class ClientDirectoryServer(RestServlet):
             yield dir_handler.create_association(
                 room_alias, room_id, servers
             )
+        except SynapseError as e:
+            raise e
         except:
             logger.exception("Failed to create association")
 
         defer.returnValue((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.",
+                               errcode=Codes.NOT_JSON)
+        return content
+    except ValueError:
+        raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
diff --git a/synapse/rest/events.py b/synapse/rest/events.py
index 2e7563d14b..7fde143200 100644
--- a/synapse/rest/events.py
+++ b/synapse/rest/events.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/rest/initial_sync.py b/synapse/rest/initial_sync.py
index d18c4c0f60..a1cb442256 100644
--- a/synapse/rest/initial_sync.py
+++ b/synapse/rest/initial_sync.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/rest/login.py b/synapse/rest/login.py
index 99e4f10aac..c7bf901c8e 100644
--- a/synapse/rest/login.py
+++ b/synapse/rest/login.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/rest/presence.py b/synapse/rest/presence.py
index bce3943542..7fc8ce4404 100644
--- a/synapse/rest/presence.py
+++ b/synapse/rest/presence.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -17,11 +17,12 @@
 """
 from twisted.internet import defer
 
+from synapse.api.errors import SynapseError
 from base import RestServlet, client_path_pattern
 
 import json
 import logging
-
+import urllib
 
 logger = logging.getLogger(__name__)
 
@@ -32,6 +33,7 @@ class PresenceStatusRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         state = yield self.handlers.presence_handler.get_state(
@@ -42,25 +44,26 @@ class PresenceStatusRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_PUT(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         state = {}
         try:
             content = json.loads(request.content.read())
 
-            # Legacy handling
-            if "state" in content:
-                state["presence"] = content.pop("state")
-            else:
-                state["presence"] = content.pop("presence")
+            state["presence"] = content.pop("presence")
 
             if "status_msg" in content:
                 state["status_msg"] = content.pop("status_msg")
+                if not isinstance(state["status_msg"], basestring):
+                    raise SynapseError(400, "status_msg must be a string.")
 
             if content:
                 raise KeyError()
+        except SynapseError as e:
+            raise e
         except:
-            defer.returnValue((400, "Unable to parse state"))
+            raise SynapseError(400, "Unable to parse state")
 
         yield self.handlers.presence_handler.set_state(
             target_user=user, auth_user=auth_user, state=state)
@@ -77,13 +80,14 @@ class PresenceListRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         if not user.is_mine:
-            defer.returnValue((400, "User not hosted on this Home Server"))
+            raise SynapseError(400, "User not hosted on this Home Server")
 
         if auth_user != user:
-            defer.returnValue((400, "Cannot get another user's presence list"))
+            raise SynapseError(400, "Cannot get another user's presence list")
 
         presence = yield self.handlers.presence_handler.get_presence_list(
             observer_user=user, accepted=True)
@@ -97,31 +101,40 @@ class PresenceListRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_POST(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         if not user.is_mine:
-            defer.returnValue((400, "User not hosted on this Home Server"))
+            raise SynapseError(400, "User not hosted on this Home Server")
 
         if auth_user != user:
-            defer.returnValue((
-                400, "Cannot modify another user's presence list"))
+            raise SynapseError(
+                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"))
+            raise SynapseError(400, "Unable to parse content")
 
         deferreds = []
 
         if "invite" in content:
             for u in content["invite"]:
+                if not isinstance(u, basestring):
+                    raise SynapseError(400, "Bad invite value.")
+                if len(u) == 0:
+                    continue
                 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"]:
+                if not isinstance(u, basestring):
+                    raise SynapseError(400, "Bad drop value.")
+                if len(u) == 0:
+                    continue
                 dropped_user = self.hs.parse_userid(u)
                 deferreds.append(self.handlers.presence_handler.drop(
                     observer_user=user, observed_user=dropped_user))
diff --git a/synapse/rest/profile.py b/synapse/rest/profile.py
index 06076667c7..2e17f87fa1 100644
--- a/synapse/rest/profile.py
+++ b/synapse/rest/profile.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ from twisted.internet import defer
 from base import RestServlet, client_path_pattern
 
 import json
+import urllib
 
 
 class ProfileDisplaynameRestServlet(RestServlet):
@@ -26,6 +27,7 @@ class ProfileDisplaynameRestServlet(RestServlet):
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         displayname = yield self.handlers.profile_handler.get_displayname(
@@ -37,6 +39,7 @@ class ProfileDisplaynameRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_PUT(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         try:
@@ -59,6 +62,7 @@ class ProfileAvatarURLRestServlet(RestServlet):
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         avatar_url = yield self.handlers.profile_handler.get_avatar_url(
@@ -70,6 +74,7 @@ class ProfileAvatarURLRestServlet(RestServlet):
     @defer.inlineCallbacks
     def on_PUT(self, request, user_id):
         auth_user = yield self.auth.get_user_by_req(request)
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         try:
@@ -92,6 +97,7 @@ class ProfileRestServlet(RestServlet):
 
     @defer.inlineCallbacks
     def on_GET(self, request, user_id):
+        user_id = urllib.unquote(user_id)
         user = self.hs.parse_userid(user_id)
 
         displayname = yield self.handlers.profile_handler.get_displayname(
diff --git a/synapse/rest/register.py b/synapse/rest/register.py
index 27ab7f182b..b8de3b250d 100644
--- a/synapse/rest/register.py
+++ b/synapse/rest/register.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/rest/room.py b/synapse/rest/room.py
index d76a2f5cd4..308b447090 100644
--- a/synapse/rest/room.py
+++ b/synapse/rest/room.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -388,7 +388,7 @@ class RoomMembershipRestServlet(RestServlet):
     def register(self, http_server):
         # /rooms/$roomid/[invite|join|leave]
         PATTERN = ("/rooms/(?P<room_id>[^/]*)/" +
-            "(?P<membership_action>join|invite|leave|ban)")
+            "(?P<membership_action>join|invite|leave|ban|kick)")
         register_txn_path(self, PATTERN, http_server)
 
     @defer.inlineCallbacks
@@ -399,11 +399,14 @@ class RoomMembershipRestServlet(RestServlet):
 
         # target user is you unless it is an invite
         state_key = user.to_string()
-        if membership_action in ["invite", "ban"]:
+        if membership_action in ["invite", "ban", "kick"]:
             if "user_id" not in content:
                 raise SynapseError(400, "Missing user_id key.")
             state_key = content["user_id"]
 
+            if membership_action == "kick":
+                membership_action = "leave"
+
         event = self.event_factory.create_event(
             etype=RoomMemberEvent.TYPE,
             content={"membership": unicode(membership_action)},
diff --git a/synapse/rest/transactions.py b/synapse/rest/transactions.py
index b8aa1ef11c..e06dcc8c57 100644
--- a/synapse/rest/transactions.py
+++ b/synapse/rest/transactions.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/server.py b/synapse/server.py
index 35e311a47d..83368ea5a7 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/state.py b/synapse/state.py
index e1a1a159bb..36d8210eb5 100644
--- a/synapse/state.py
+++ b/synapse/state.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -179,6 +179,18 @@ class StateHandler(object):
                 key=lambda x: x.depth
             )
 
+            if not hasattr(missing_prev, "prev_state_id"):
+                # FIXME Hmm
+                # temporary fallback
+                for algo in conflict_res:
+                    new_res, curr_res = algo(new_branch, current_branch)
+
+                    if new_res < curr_res:
+                        defer.returnValue(False)
+                    elif new_res > curr_res:
+                        defer.returnValue(True)
+                return
+
             pdu_id = missing_prev.prev_state_id
             origin = missing_prev.prev_state_origin
 
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index aadaab06e7..d97014f4da 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index 33d56f47ce..bae50e7d1f 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -79,19 +79,21 @@ class SQLBaseStore(object):
     # "Simple" SQL API methods that operate on a single table with no JOINs,
     # no complex WHERE clauses, just a dict of values for columns.
 
-    def _simple_insert(self, table, values):
+    def _simple_insert(self, table, values, or_replace=False):
         """Executes an INSERT query on the named table.
 
         Args:
             table : string giving the table name
             values : dict of new column names and values for them
+            or_replace : bool; if True performs an INSERT OR REPLACE
         """
         return self._db_pool.runInteraction(
-            self._simple_insert_txn, table, values,
+            self._simple_insert_txn, table, values, or_replace=or_replace
         )
 
-    def _simple_insert_txn(self, txn, table, values):
-        sql = "INSERT INTO %s (%s) VALUES(%s)" % (
+    def _simple_insert_txn(self, txn, table, values, or_replace=False):
+        sql = "%s INTO %s (%s) VALUES(%s)" % (
+            ("INSERT OR REPLACE" if or_replace else "INSERT"),
             table,
             ", ".join(k for k in values),
             ", ".join("?" for k in values)
diff --git a/synapse/storage/directory.py b/synapse/storage/directory.py
index b22ce02f3f..bf55449253 100644
--- a/synapse/storage/directory.py
+++ b/synapse/storage/directory.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/feedback.py b/synapse/storage/feedback.py
index bac3dea955..8a18617188 100644
--- a/synapse/storage/feedback.py
+++ b/synapse/storage/feedback.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/keys.py b/synapse/storage/keys.py
index 4d19b9f641..5a38c3e8f2 100644
--- a/synapse/storage/keys.py
+++ b/synapse/storage/keys.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/pdu.py b/synapse/storage/pdu.py
index 9fd44f2454..0bf97e37ee 100644
--- a/synapse/storage/pdu.py
+++ b/synapse/storage/pdu.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/presence.py b/synapse/storage/presence.py
index a529104f4d..71b2bb084d 100644
--- a/synapse/storage/presence.py
+++ b/synapse/storage/presence.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/profile.py b/synapse/storage/profile.py
index 91dd565033..7e1fdd9d88 100644
--- a/synapse/storage/profile.py
+++ b/synapse/storage/profile.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py
index b1e4196435..fd762bc643 100644
--- a/synapse/storage/registration.py
+++ b/synapse/storage/registration.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/room.py b/synapse/storage/room.py
index 01ae190316..017169ce00 100644
--- a/synapse/storage/room.py
+++ b/synapse/storage/room.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py
index 2746126e85..75c9a60101 100644
--- a/synapse/storage/roommember.py
+++ b/synapse/storage/roommember.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -165,7 +165,7 @@ class RoomMemberStore(SQLBaseStore):
         defer.returnValue(results)
 
     @defer.inlineCallbacks
-    def do_users_share_a_room(self, user_list):
+    def user_rooms_intersect(self, user_list):
         """ Checks whether a list of users share a room.
         """
         user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_list))
diff --git a/synapse/storage/schema/delta/v2.sql b/synapse/storage/schema/delta/v2.sql
new file mode 100644
index 0000000000..73b140465e
--- /dev/null
+++ b/synapse/storage/schema/delta/v2.sql
@@ -0,0 +1,168 @@
+/* Copyright 2014 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.
+ */
+
+CREATE TABLE IF NOT EXISTS events(
+    stream_ordering INTEGER PRIMARY KEY AUTOINCREMENT,
+    topological_ordering INTEGER NOT NULL,
+    event_id TEXT NOT NULL,
+    type TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    content TEXT NOT NULL,
+    unrecognized_keys TEXT,
+    processed BOOL NOT NULL,
+    outlier BOOL NOT NULL,
+    CONSTRAINT ev_uniq UNIQUE (event_id)
+);
+
+CREATE INDEX IF NOT EXISTS events_event_id ON events (event_id);
+CREATE INDEX IF NOT EXISTS events_stream_ordering ON events (stream_ordering);
+CREATE INDEX IF NOT EXISTS events_topological_ordering ON events (topological_ordering);
+CREATE INDEX IF NOT EXISTS events_room_id ON events (room_id);
+
+CREATE TABLE IF NOT EXISTS state_events(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    type TEXT NOT NULL,
+    state_key TEXT NOT NULL,
+    prev_state TEXT
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS state_events_event_id ON state_events (event_id);
+CREATE INDEX IF NOT EXISTS state_events_room_id ON state_events (room_id);
+CREATE INDEX IF NOT EXISTS state_events_type ON state_events (type);
+CREATE INDEX IF NOT EXISTS state_events_state_key ON state_events (state_key);
+
+
+CREATE TABLE IF NOT EXISTS current_state_events(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    type TEXT NOT NULL,
+    state_key TEXT NOT NULL,
+    CONSTRAINT curr_uniq UNIQUE (room_id, type, state_key) ON CONFLICT REPLACE
+);
+
+CREATE INDEX IF NOT EXISTS curr_events_event_id ON current_state_events (event_id);
+CREATE INDEX IF NOT EXISTS current_state_events_room_id ON current_state_events (room_id);
+CREATE INDEX IF NOT EXISTS current_state_events_type ON current_state_events (type);
+CREATE INDEX IF NOT EXISTS current_state_events_state_key ON current_state_events (state_key);
+
+CREATE TABLE IF NOT EXISTS room_memberships(
+    event_id TEXT NOT NULL,
+    user_id TEXT NOT NULL,
+    sender TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    membership TEXT NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS room_memberships_event_id ON room_memberships (event_id);
+CREATE INDEX IF NOT EXISTS room_memberships_room_id ON room_memberships (room_id);
+CREATE INDEX IF NOT EXISTS room_memberships_user_id ON room_memberships (user_id);
+
+CREATE TABLE IF NOT EXISTS feedback(
+    event_id TEXT NOT NULL,
+    feedback_type TEXT,
+    target_event_id TEXT,
+    sender TEXT,
+    room_id TEXT
+);
+
+CREATE TABLE IF NOT EXISTS topics(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    topic TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS room_names(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    name TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS rooms(
+    room_id TEXT PRIMARY KEY NOT NULL,
+    is_public INTEGER,
+    creator TEXT
+);
+
+CREATE TABLE IF NOT EXISTS room_join_rules(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    join_rule TEXT NOT NULL
+);
+CREATE INDEX IF NOT EXISTS room_join_rules_event_id ON room_join_rules(event_id);
+CREATE INDEX IF NOT EXISTS room_join_rules_room_id ON room_join_rules(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_power_levels(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    user_id TEXT NOT NULL,
+    level INTEGER NOT NULL
+);
+CREATE INDEX IF NOT EXISTS room_power_levels_event_id ON room_power_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_power_levels_room_id ON room_power_levels(room_id);
+CREATE INDEX IF NOT EXISTS room_power_levels_room_user ON room_power_levels(room_id, user_id);
+
+
+CREATE TABLE IF NOT EXISTS room_default_levels(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    level INTEGER NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS room_default_levels_event_id ON room_default_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_default_levels_room_id ON room_default_levels(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_add_state_levels(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    level INTEGER NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS room_add_state_levels_event_id ON room_add_state_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_add_state_levels_room_id ON room_add_state_levels(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_send_event_levels(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    level INTEGER NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS room_send_event_levels_event_id ON room_send_event_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_send_event_levels_room_id ON room_send_event_levels(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_ops_levels(
+    event_id TEXT NOT NULL,
+    room_id TEXT NOT NULL,
+    ban_level INTEGER,
+    kick_level INTEGER
+);
+
+CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id);
+CREATE INDEX IF NOT EXISTS room_ops_levels_room_id ON room_ops_levels(room_id);
+
+
+CREATE TABLE IF NOT EXISTS room_hosts(
+    room_id TEXT NOT NULL,
+    host TEXT NOT NULL,
+    CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
+);
+
+CREATE INDEX IF NOT EXISTS room_hosts_room_id ON room_hosts (room_id);
+
+PRAGMA user_version = 2;
diff --git a/synapse/storage/schema/edge_pdus.sql b/synapse/storage/schema/edge_pdus.sql
index 17b3c52f0d..8a00868065 100644
--- a/synapse/storage/schema/edge_pdus.sql
+++ b/synapse/storage/schema/edge_pdus.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql
index dbefbbda31..6ffea51310 100644
--- a/synapse/storage/schema/im.sql
+++ b/synapse/storage/schema/im.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/schema/keys.sql b/synapse/storage/schema/keys.sql
index 45cdbcecae..706a1a03ff 100644
--- a/synapse/storage/schema/keys.sql
+++ b/synapse/storage/schema/keys.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/schema/pdu.sql b/synapse/storage/schema/pdu.sql
index ca3de005e9..16e111a56c 100644
--- a/synapse/storage/schema/pdu.sql
+++ b/synapse/storage/schema/pdu.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/schema/presence.sql b/synapse/storage/schema/presence.sql
index b1081d3aab..595b3b5a69 100644
--- a/synapse/storage/schema/presence.sql
+++ b/synapse/storage/schema/presence.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/schema/profiles.sql b/synapse/storage/schema/profiles.sql
index 1092d7672c..58209f1af0 100644
--- a/synapse/storage/schema/profiles.sql
+++ b/synapse/storage/schema/profiles.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/schema/room_aliases.sql b/synapse/storage/schema/room_aliases.sql
index d72b79e6ed..9191016814 100644
--- a/synapse/storage/schema/room_aliases.sql
+++ b/synapse/storage/schema/room_aliases.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/schema/transactions.sql b/synapse/storage/schema/transactions.sql
index 4b1a2368f6..88e3e4e04d 100644
--- a/synapse/storage/schema/transactions.sql
+++ b/synapse/storage/schema/transactions.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/schema/users.sql b/synapse/storage/schema/users.sql
index 46b60297cb..2519702971 100644
--- a/synapse/storage/schema/users.sql
+++ b/synapse/storage/schema/users.sql
@@ -1,4 +1,4 @@
-/* Copyright 2014 matrix.org
+/* Copyright 2014 OpenMarket Ltd
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py
index 0b78222827..2cb0067a67 100644
--- a/synapse/storage/stream.py
+++ b/synapse/storage/stream.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/storage/transactions.py b/synapse/storage/transactions.py
index a277e4971a..7467e1035b 100644
--- a/synapse/storage/transactions.py
+++ b/synapse/storage/transactions.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/streams/__init__.py b/synapse/streams/__init__.py
index fe8a073cd3..f9811bfa04 100644
--- a/synapse/streams/__init__.py
+++ b/synapse/streams/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/streams/config.py b/synapse/streams/config.py
index 01bab568ff..6483ce2e25 100644
--- a/synapse/streams/config.py
+++ b/synapse/streams/config.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/streams/events.py b/synapse/streams/events.py
index 08d6e6f733..41715436b0 100644
--- a/synapse/streams/events.py
+++ b/synapse/streams/events.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/types.py b/synapse/types.py
index 1a9dceabf5..c51bc8e4f2 100644
--- a/synapse/types.py
+++ b/synapse/types.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/util/__init__.py b/synapse/util/__init__.py
index 3ea431a7f9..c9a73b0413 100644
--- a/synapse/util/__init__.py
+++ b/synapse/util/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/util/async.py b/synapse/util/async.py
index ebbdc00ae1..647ea6142c 100644
--- a/synapse/util/async.py
+++ b/synapse/util/async.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/util/distributor.py b/synapse/util/distributor.py
index 9605d7d1b9..1de50e049f 100644
--- a/synapse/util/distributor.py
+++ b/synapse/util/distributor.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -32,7 +32,9 @@ class Distributor(object):
       model will do for today.
     """
 
-    def __init__(self):
+    def __init__(self, suppress_failures=True):
+        self.suppress_failures = suppress_failures
+
         self.signals = {}
         self.pre_registration = {}
 
@@ -40,7 +42,9 @@ class Distributor(object):
         if name in self.signals:
             raise KeyError("%r already has a signal named %s" % (self, name))
 
-        self.signals[name] = Signal(name)
+        self.signals[name] = Signal(name,
+            suppress_failures=self.suppress_failures,
+        )
 
         if name in self.pre_registration:
             signal = self.signals[name]
@@ -74,8 +78,9 @@ class Signal(object):
     method into all of the observers.
     """
 
-    def __init__(self, name):
+    def __init__(self, name, suppress_failures):
         self.name = name
+        self.suppress_failures = suppress_failures
         self.observers = []
 
     def observe(self, observer):
@@ -104,6 +109,10 @@ class Signal(object):
                         failure.type,
                         failure.value,
                         failure.getTracebackObject()))
+                if not self.suppress_failures:
+                    raise failure
             deferreds.append(d.addErrback(eb))
 
-        return defer.DeferredList(deferreds)
+        return defer.DeferredList(
+            deferreds, fireOnOneErrback=not self.suppress_failures
+        )
diff --git a/synapse/util/jsonobject.py b/synapse/util/jsonobject.py
index e2840b59f9..6c99705747 100644
--- a/synapse/util/jsonobject.py
+++ b/synapse/util/jsonobject.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/util/lockutils.py b/synapse/util/lockutils.py
index d0bb50d035..3a84c09db4 100644
--- a/synapse/util/lockutils.py
+++ b/synapse/util/lockutils.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/util/logutils.py b/synapse/util/logutils.py
index 3d7c46ae44..fadf0bd510 100644
--- a/synapse/util/logutils.py
+++ b/synapse/util/logutils.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py
index e1b0796e56..8767e437dd 100644
--- a/synapse/util/stringutils.py
+++ b/synapse/util/stringutils.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/__init__.py b/tests/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/events/__init__.py b/tests/events/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/tests/events/__init__.py
+++ b/tests/events/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/events/test_events.py b/tests/events/test_events.py
index 35e9c68f5c..93d5c15c6f 100644
--- a/tests/events/test_events.py
+++ b/tests/events/test_events.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/federation/test_federation.py b/tests/federation/test_federation.py
index 51308ca358..0b105fe723 100644
--- a/tests/federation/test_federation.py
+++ b/tests/federation/test_federation.py
@@ -1,4 +1,4 @@
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/federation/test_pdu_codec.py b/tests/federation/test_pdu_codec.py
index 2c546040b8..9f74ba119f 100644
--- a/tests/federation/test_pdu_codec.py
+++ b/tests/federation/test_pdu_codec.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py
index 88ac8933f8..72a2b1443a 100644
--- a/tests/handlers/test_directory.py
+++ b/tests/handlers/test_directory.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ from mock import Mock
 import logging
 
 from synapse.server import HomeServer
+from synapse.http.client import HttpClient
 from synapse.handlers.directory import DirectoryHandler
 from synapse.storage.directory import RoomAliasMapping
 
@@ -49,6 +50,7 @@ class DirectoryTestCase(unittest.TestCase):
         hs = HomeServer("test",
             datastore=Mock(spec=[
                 "get_association_from_room_alias",
+                "get_joined_hosts_for_room",
             ]),
             http_client=None,
             resource_for_federation=Mock(),
@@ -60,6 +62,10 @@ class DirectoryTestCase(unittest.TestCase):
 
         self.datastore = hs.get_datastore()
 
+        def hosts(room_id):
+            return defer.succeed([])
+        self.datastore.get_joined_hosts_for_room.side_effect = hosts
+
         self.my_room = hs.parse_roomalias("#my-room:test")
         self.remote_room = hs.parse_roomalias("#another:remote")
 
@@ -92,7 +98,10 @@ class DirectoryTestCase(unittest.TestCase):
         self.mock_federation.make_query.assert_called_with(
             destination="remote",
             query_type="directory",
-            args={"room_alias": "#another:remote"}
+            args={
+                "room_alias": "#another:remote",
+                HttpClient.RETRY_DNS_LOOKUP_FAILURES: False
+            }
         )
 
     @defer.inlineCallbacks
diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py
index fd19442645..7c4921e226 100644
--- a/tests/handlers/test_federation.py
+++ b/tests/handlers/test_federation.py
@@ -1,4 +1,4 @@
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index b8309bc063..9eb8b6909f 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -117,10 +117,12 @@ class PresenceStateTestCase(unittest.TestCase):
                 return defer.succeed([])
         room_member_handler.get_room_members = get_room_members
 
-        def do_users_share_a_room(userlist):
-            shared = all(map(lambda u: u in self.room_members, userlist))
+        def user_rooms_intersect(userlist):
+            room_member_ids = map(lambda u: u.to_string(), self.room_members)
+
+            shared = all(map(lambda i: i in room_member_ids, userlist))
             return defer.succeed(shared)
-        self.datastore.do_users_share_a_room = do_users_share_a_room
+        self.datastore.user_rooms_intersect = user_rooms_intersect
 
         self.mock_start = Mock()
         self.mock_stop = Mock()
@@ -140,7 +142,7 @@ class PresenceStateTestCase(unittest.TestCase):
         )
 
         self.assertEquals(
-            {"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
+            {"presence": ONLINE, "status_msg": "Online"},
             state
         )
         mocked_get.assert_called_with("apple")
@@ -157,7 +159,7 @@ class PresenceStateTestCase(unittest.TestCase):
         )
 
         self.assertEquals(
-            {"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
+            {"presence": ONLINE, "status_msg": "Online"},
             state
         )
         mocked_get.assert_called_with("apple")
@@ -176,7 +178,7 @@ class PresenceStateTestCase(unittest.TestCase):
         )
 
         self.assertEquals(
-            {"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
+            {"presence": ONLINE, "status_msg": "Online"},
             state
         )
 
@@ -206,7 +208,8 @@ class PresenceStateTestCase(unittest.TestCase):
                 state={"presence": UNAVAILABLE, "status_msg": "Away"})
 
         mocked_set.assert_called_with("apple",
-                {"state": UNAVAILABLE, "status_msg": "Away"})
+            {"state": UNAVAILABLE, "status_msg": "Away"}
+        )
         self.mock_start.assert_called_with(self.u_apple,
                 state={
                     "presence": UNAVAILABLE,
@@ -458,8 +461,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals([
             {"observed_user": self.u_banana,
-             "presence": OFFLINE,
-             "state": OFFLINE},
+             "presence": OFFLINE},
         ], presence)
 
         self.datastore.get_presence_list.assert_called_with("apple",
@@ -476,8 +478,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
 
         self.assertEquals([
             {"observed_user": self.u_banana,
-             "presence": OFFLINE,
-             "state": OFFLINE},
+             "presence": OFFLINE},
         ], presence)
 
         self.datastore.get_presence_list.assert_called_with("apple",
@@ -562,6 +563,13 @@ class PresencePushTestCase(unittest.TestCase):
                 return defer.succeed([])
         self.datastore.get_joined_hosts_for_room = get_room_hosts
 
+        def user_rooms_intersect(userlist):
+            room_member_ids = map(lambda u: u.to_string(), self.room_members)
+
+            shared = all(map(lambda i: i in room_member_ids, userlist))
+            return defer.succeed(shared)
+        self.datastore.user_rooms_intersect = user_rooms_intersect
+
         @defer.inlineCallbacks
         def fetch_room_distributions_into(room_id, localusers=None,
                 remotedomains=None, ignore_user=None):
@@ -604,6 +612,7 @@ class PresencePushTestCase(unittest.TestCase):
         self.u_apple = hs.parse_userid("@apple:test")
         self.u_banana = hs.parse_userid("@banana:test")
         self.u_clementine = hs.parse_userid("@clementine:test")
+        self.u_durian = hs.parse_userid("@durian:test")
         self.u_elderberry = hs.parse_userid("@elderberry:test")
 
         # Remote user
@@ -615,7 +624,8 @@ class PresencePushTestCase(unittest.TestCase):
         self.room_members = [self.u_apple, self.u_elderberry]
 
         self.datastore.set_presence_state.return_value = defer.succeed(
-                {"state": ONLINE})
+            {"state": ONLINE}
+        )
 
         # TODO(paul): Gut-wrenching
         self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
@@ -632,6 +642,7 @@ class PresencePushTestCase(unittest.TestCase):
             {"presence": ONLINE}
         )
 
+        # Apple sees self-reflection
         (events, _) = yield self.event_source.get_new_events_for_user(
             self.u_apple, 0, None
         )
@@ -643,10 +654,56 @@ class PresencePushTestCase(unittest.TestCase):
                  "content": {
                     "user_id": "@apple:test",
                     "presence": ONLINE,
-                    "state": ONLINE,
                     "last_active_ago": 0,
                 }},
             ],
+            msg="Presence event should be visible to self-reflection"
+        )
+
+        # Banana sees it because of presence subscription
+        (events, _) = yield self.event_source.get_new_events_for_user(
+            self.u_banana, 0, None
+        )
+
+        self.assertEquals(self.event_source.get_current_key(), 1)
+        self.assertEquals(events,
+            [
+                {"type": "m.presence",
+                 "content": {
+                    "user_id": "@apple:test",
+                    "presence": ONLINE,
+                    "last_active_ago": 0,
+                }},
+            ],
+            msg="Presence event should be visible to explicit subscribers"
+        )
+
+        # Elderberry sees it because of same room
+        (events, _) = yield self.event_source.get_new_events_for_user(
+            self.u_elderberry, 0, None
+        )
+
+        self.assertEquals(self.event_source.get_current_key(), 1)
+        self.assertEquals(events,
+            [
+                {"type": "m.presence",
+                 "content": {
+                    "user_id": "@apple:test",
+                    "presence": ONLINE,
+                    "last_active_ago": 0,
+                }},
+            ],
+            msg="Presence event should be visible to other room members"
+        )
+
+        # Durian is not in the room, should not see this event
+        (events, _) = yield self.event_source.get_new_events_for_user(
+            self.u_durian, 0, None
+        )
+
+        self.assertEquals(self.event_source.get_current_key(), 1)
+        self.assertEquals(events, [],
+            msg="Presence event should not be visible to others"
         )
 
         presence = yield self.handler.get_presence_list(
@@ -655,15 +712,17 @@ class PresencePushTestCase(unittest.TestCase):
         self.assertEquals(
             [
                 {"observed_user": self.u_banana, 
-                 "presence": OFFLINE,
-                 "state": OFFLINE},
+                 "presence": OFFLINE},
                 {"observed_user": self.u_clementine,
-                 "presence": OFFLINE,
-                 "state": OFFLINE},
+                 "presence": OFFLINE},
             ],
             presence
         )
 
+        # TODO(paul): Gut-wrenching
+        banana_set = self.handler._local_pushmap.setdefault("banana", set())
+        banana_set.add(self.u_apple)
+
         yield self.handler.set_state(self.u_banana, self.u_banana,
             {"presence": ONLINE}
         )
@@ -676,11 +735,9 @@ class PresencePushTestCase(unittest.TestCase):
         self.assertEquals([
                 {"observed_user": self.u_banana,
                  "presence": ONLINE,
-                 "state": ONLINE,
                  "last_active_ago": 2000},
                 {"observed_user": self.u_clementine,
-                 "presence": OFFLINE,
-                 "state": OFFLINE},
+                 "presence": OFFLINE},
         ], presence)
 
         (events, _) = yield self.event_source.get_new_events_for_user(
@@ -694,7 +751,6 @@ class PresencePushTestCase(unittest.TestCase):
                  "content": {
                      "user_id": "@banana:test",
                      "presence": ONLINE,
-                     "state": ONLINE,
                      "last_active_ago": 2000
                 }},
             ]
@@ -711,7 +767,21 @@ class PresencePushTestCase(unittest.TestCase):
                         "push": [
                             {"user_id": "@apple:test",
                              "presence": u"online",
-                             "state": u"online",
+                             "last_active_ago": 0},
+                        ],
+                    }
+                )
+            ),
+            defer.succeed((200, "OK"))
+        )
+        put_json.expect_call_and_return(
+            call("remote",
+                path=ANY,  # Can't guarantee which txn ID will be which
+                data=_expect_edu("remote", "m.presence",
+                    content={
+                        "push": [
+                            {"user_id": "@apple:test",
+                             "presence": u"online",
                              "last_active_ago": 0},
                         ],
                     }
@@ -757,7 +827,7 @@ class PresencePushTestCase(unittest.TestCase):
                 content={
                     "push": [
                         {"user_id": "@potato:remote",
-                         "state": "online",
+                         "presence": "online",
                          "last_active_ago": 1000},
                     ],
                 }
@@ -775,7 +845,6 @@ class PresencePushTestCase(unittest.TestCase):
                  "content": {
                      "user_id": "@potato:remote",
                      "presence": ONLINE,
-                     "state": ONLINE,
                      "last_active_ago": 1000,
                 }}
             ]
@@ -786,7 +855,7 @@ class PresencePushTestCase(unittest.TestCase):
         state = yield self.handler.get_state(self.u_potato, self.u_apple)
 
         self.assertEquals(
-            {"state": ONLINE, "presence": ONLINE, "last_active_ago": 3000},
+            {"presence": ONLINE, "last_active_ago": 3000},
             state
         )
 
@@ -809,6 +878,8 @@ class PresencePushTestCase(unittest.TestCase):
             "a-room"
         )
 
+        self.room_members.append(self.u_clementine)
+
         (events, _) = yield self.event_source.get_new_events_for_user(
             self.u_apple, 0, None
         )
@@ -820,7 +891,6 @@ class PresencePushTestCase(unittest.TestCase):
                  "content": {
                      "user_id": "@clementine:test",
                      "presence": ONLINE,
-                     "state": ONLINE,
                      "last_active_ago": 0,
                 }}
             ]
@@ -837,8 +907,7 @@ class PresencePushTestCase(unittest.TestCase):
                     content={
                         "push": [
                             {"user_id": "@apple:test",
-                             "presence": "online",
-                             "state": "online"},
+                             "presence": "online"},
                         ],
                     }
                 ),
@@ -852,8 +921,7 @@ class PresencePushTestCase(unittest.TestCase):
                     content={
                         "push": [
                             {"user_id": "@banana:test",
-                             "presence": "offline",
-                             "state": "offline"},
+                             "presence": "offline"},
                         ],
                     }
                 ),
@@ -882,8 +950,7 @@ class PresencePushTestCase(unittest.TestCase):
                     content={
                         "push": [
                             {"user_id": "@clementine:test",
-                             "presence": "online",
-                             "state": "online"},
+                             "presence": "online"},
                         ],
                     }
                 ),
@@ -1182,7 +1249,6 @@ class PresencePollingTestCase(unittest.TestCase):
                         "push": [
                             {"user_id": "@banana:test",
                              "presence": "offline",
-                             "state": "offline",
                              "status_msg": None},
                         ],
                     },
diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py
index 38cc34350b..b35980d948 100644
--- a/tests/handlers/test_presencelike.py
+++ b/tests/handlers/test_presencelike.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -107,9 +107,9 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
             return defer.succeed(self.presence_list)
         self.datastore.get_presence_list = get_presence_list
 
-        def do_users_share_a_room(userlist):
+        def user_rooms_intersect(userlist):
             return defer.succeed(False)
-        self.datastore.do_users_share_a_room = do_users_share_a_room
+        self.datastore.user_rooms_intersect = user_rooms_intersect
 
         self.handlers = hs.get_handlers()
 
@@ -148,10 +148,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
 
         yield self.handlers.presence_handler.set_state(
                 target_user=self.u_apple, auth_user=self.u_apple,
-                state={"state": UNAVAILABLE, "status_msg": "Away"})
+                state={"presence": UNAVAILABLE, "status_msg": "Away"})
 
         mocked_set.assert_called_with("apple",
-                {"state": UNAVAILABLE, "status_msg": "Away"})
+            {"state": UNAVAILABLE, "status_msg": "Away"}
+        )
 
     @defer.inlineCallbacks
     def test_push_local(self):
@@ -161,7 +162,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
         ]
 
         self.datastore.set_presence_state.return_value = defer.succeed(
-                {"state": ONLINE})
+            {"state": ONLINE}
+        )
 
         # TODO(paul): Gut-wrenching
         from synapse.handlers.presence import UserPresenceCache
@@ -177,9 +179,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
         apple_set.add(self.u_clementine)
 
         yield self.handlers.presence_handler.set_state(self.u_apple,
-                self.u_apple, {"state": ONLINE})
+            self.u_apple, {"presence": ONLINE}
+        )
         yield self.handlers.presence_handler.set_state(self.u_banana,
-                self.u_banana, {"state": ONLINE})
+            self.u_banana, {"presence": ONLINE}
+        )
 
         presence = yield self.handlers.presence_handler.get_presence_list(
                 observer_user=self.u_apple, accepted=True)
@@ -187,14 +191,12 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
         self.assertEquals([
             {"observed_user": self.u_banana,
                 "presence": ONLINE,
-                "state": ONLINE,
                 "last_active_ago": 0,
                 "displayname": "Frank",
                 "avatar_url": "http://foo"},
             {"observed_user": self.u_clementine,
-                "presence": OFFLINE,
-                "state": OFFLINE}],
-        presence)
+                "presence": OFFLINE}
+        ], presence)
 
         self.mock_update_client.assert_has_calls([
             call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
@@ -242,7 +244,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
         ]
 
         self.datastore.set_presence_state.return_value = defer.succeed(
-                {"state": ONLINE})
+            {"state": ONLINE}
+        )
 
         # TODO(paul): Gut-wrenching
         from synapse.handlers.presence import UserPresenceCache
@@ -257,7 +260,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
         apple_set.add(self.u_potato.domain)
 
         yield self.handlers.presence_handler.set_state(self.u_apple,
-                self.u_apple, {"state": ONLINE})
+            self.u_apple, {"presence": ONLINE}
+        )
 
         self.replication.send_edu.assert_called_with(
                 destination="remote",
@@ -266,7 +270,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
                     "push": [
                         {"user_id": "@apple:test",
                          "presence": "online",
-                         "state": "online",
                          "last_active_ago": 0,
                          "displayname": "Frank",
                          "avatar_url": "http://foo"},
@@ -283,18 +286,19 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
 
         # TODO(paul): Gut-wrenching
         potato_set = self.handlers.presence_handler._remote_recvmap.setdefault(
-                self.u_potato, set())
+            self.u_potato, set()
+        )
         potato_set.add(self.u_apple)
 
         yield self.replication.received_edu(
-                "remote", "m.presence", {
-                    "push": [
-                        {"user_id": "@potato:remote",
-                         "state": "online",
-                         "displayname": "Frank",
-                         "avatar_url": "http://foo"},
-                    ],
-                }
+            "remote", "m.presence", {
+                "push": [
+                    {"user_id": "@potato:remote",
+                     "presence": "online",
+                     "displayname": "Frank",
+                     "avatar_url": "http://foo"},
+                ],
+            }
         )
 
         self.mock_update_client.assert_called_with(
@@ -313,7 +317,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
 
         self.assertEquals(
                 {"presence": ONLINE,
-                 "state": ONLINE,
                  "displayname": "Frank",
                  "avatar_url": "http://foo"},
             state)
diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py
index 87a8139920..8e7a89b479 100644
--- a/tests/handlers/test_profile.py
+++ b/tests/handlers/test_profile.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py
index 4591a5ea58..5687bbea0b 100644
--- a/tests/handlers/test_room.py
+++ b/tests/handlers/test_room.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py
index c3c98074cc..6532ac94a3 100644
--- a/tests/handlers/test_typing.py
+++ b/tests/handlers/test_typing.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/rest/__init__.py b/tests/rest/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/tests/rest/__init__.py
+++ b/tests/rest/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/rest/test_events.py b/tests/rest/test_events.py
index c8527f3517..1dccf4c503 100644
--- a/tests/rest/test_events.py
+++ b/tests/rest/test_events.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py
index e2cdd80e07..a1db0fbcf3 100644
--- a/tests/rest/test_presence.py
+++ b/tests/rest/test_presence.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -99,7 +99,7 @@ class PresenceStateTestCase(unittest.TestCase):
 
         self.assertEquals(200, code)
         self.assertEquals(
-            {"presence": ONLINE, "state": ONLINE, "status_msg": "Available"},
+            {"presence": ONLINE, "status_msg": "Available"},
             response
         )
         mocked_get.assert_called_with("apple")
@@ -115,7 +115,8 @@ class PresenceStateTestCase(unittest.TestCase):
 
         self.assertEquals(200, code)
         mocked_set.assert_called_with("apple",
-                {"state": UNAVAILABLE, "status_msg": "Away"})
+            {"state": UNAVAILABLE, "status_msg": "Away"}
+        )
 
 
 class PresenceListTestCase(unittest.TestCase):
@@ -176,7 +177,7 @@ class PresenceListTestCase(unittest.TestCase):
 
         self.assertEquals(200, code)
         self.assertEquals([
-            {"user_id": "@banana:test", "presence": OFFLINE, "state": OFFLINE},
+            {"user_id": "@banana:test", "presence": OFFLINE},
         ], response)
 
         self.datastore.get_presence_list.assert_called_with(
@@ -269,11 +270,16 @@ class PresenceEventStreamTestCase(unittest.TestCase):
 
         hs.register_servlets()
 
-        hs.handlers.room_member_handler = Mock(spec=[
-            "get_rooms_for_user",
-        ])
-        hs.handlers.room_member_handler.get_rooms_for_user = (
-                lambda u: defer.succeed([]))
+        hs.handlers.room_member_handler = Mock(spec=[])
+
+        self.room_members = []
+
+        def get_rooms_for_user(user):
+            if user in self.room_members:
+                return ["a-room"]
+            else:
+                return []
+        hs.handlers.room_member_handler.get_rooms_for_user = get_rooms_for_user
 
         self.mock_datastore = hs.get_datastore()
 
@@ -285,6 +291,17 @@ class PresenceEventStreamTestCase(unittest.TestCase):
             return defer.succeed(None)
         self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url
 
+        def user_rooms_intersect(user_list):
+            room_member_ids = map(lambda u: u.to_string(), self.room_members)
+
+            shared = all(map(lambda i: i in room_member_ids, user_list))
+            return defer.succeed(shared)
+        self.mock_datastore.user_rooms_intersect = user_rooms_intersect
+
+        def get_joined_hosts_for_room(room_id):
+            return []
+        self.mock_datastore.get_joined_hosts_for_room = get_joined_hosts_for_room
+
         self.presence = hs.get_handlers().presence_handler
 
         self.u_apple = hs.parse_userid("@apple:test")
@@ -292,10 +309,14 @@ class PresenceEventStreamTestCase(unittest.TestCase):
 
     @defer.inlineCallbacks
     def test_shortpoll(self):
+        self.room_members = [self.u_apple, self.u_banana]
+
         self.mock_datastore.set_presence_state.return_value = defer.succeed(
-                {"state": ONLINE})
+            {"state": ONLINE}
+        )
         self.mock_datastore.get_presence_list.return_value = defer.succeed(
-                [])
+            []
+        )
 
         (code, response) = yield self.mock_resource.trigger("GET",
                 "/events?timeout=0", None)
@@ -311,9 +332,11 @@ class PresenceEventStreamTestCase(unittest.TestCase):
         )
 
         self.mock_datastore.set_presence_state.return_value = defer.succeed(
-                {"state": ONLINE})
+            {"state": ONLINE}
+        )
         self.mock_datastore.get_presence_list.return_value = defer.succeed(
-                [])
+            []
+        )
 
         yield self.presence.set_state(self.u_banana, self.u_banana,
             state={"presence": ONLINE}
@@ -328,7 +351,6 @@ class PresenceEventStreamTestCase(unittest.TestCase):
              "content": {
                  "user_id": "@banana:test",
                  "presence": ONLINE,
-                 "state": ONLINE,
                  "displayname": "Frank",
                  "last_active_ago": 0,
             }},
diff --git a/tests/rest/test_profile.py b/tests/rest/test_profile.py
index 24456769c7..f41810df1f 100644
--- a/tests/rest/test_profile.py
+++ b/tests/rest/test_profile.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/rest/test_rooms.py b/tests/rest/test_rooms.py
index 23c50824c7..4ea5828d4f 100644
--- a/tests/rest/test_rooms.py
+++ b/tests/rest/test_rooms.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/rest/utils.py b/tests/rest/utils.py
index ef9a6071e2..77f5ecf0df 100644
--- a/tests/rest/utils.py
+++ b/tests/rest/utils.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py
index 5567480921..330311448d 100644
--- a/tests/storage/test_base.py
+++ b/tests/storage/test_base.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/test_distributor.py b/tests/test_distributor.py
index 21c91f335b..04933f0ecf 100644
--- a/tests/test_distributor.py
+++ b/tests/test_distributor.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -13,9 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
-
 from twisted.internet import defer
+from twisted.trial import unittest
 
 from mock import Mock, patch
 
@@ -75,6 +74,24 @@ class DistributorTestCase(unittest.TestCase):
             self.assertIsInstance(mock_logger.warning.call_args[0][0],
                     str)
 
+    @defer.inlineCallbacks
+    def test_signal_catch_no_suppress(self):
+        # Gut-wrenching
+        self.dist.suppress_failures = False
+
+        self.dist.declare("whail")
+
+        observer = Mock()
+        observer.return_value = defer.fail(
+            Exception("Oopsie")
+        )
+
+        self.dist.observe("whail", observer)
+
+        d = self.dist.fire("whail")
+
+        yield self.assertFailure(d, Exception)
+
     def test_signal_prereg(self):
         observer = Mock()
         self.dist.observe("flare", observer)
@@ -85,5 +102,6 @@ class DistributorTestCase(unittest.TestCase):
         observer.assert_called_with(4, 5)
 
     def test_signal_undeclared(self):
-        with self.assertRaises(KeyError):
+        def code():
             self.dist.fire("notification")
+        self.assertRaises(KeyError, code)
diff --git a/tests/test_state.py b/tests/test_state.py
index 58fd0bf3be..a1f5ee869b 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/test_types.py b/tests/test_types.py
index d2ccbcfa55..571938356c 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/util/__init__.py b/tests/util/__init__.py
index 2216c0f1ca..9bff9ec169 100644
--- a/tests/util/__init__.py
+++ b/tests/util/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/util/test_lock.py b/tests/util/test_lock.py
index dd83d204d9..5623d78423 100644
--- a/tests/util/test_lock.py
+++ b/tests/util/test_lock.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/tests/utils.py b/tests/utils.py
index aa7e499e15..d90214e418 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
+# Copyright 2014 OpenMarket Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index a77d32a5ac..ea48cbb011 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/app-directive.js b/webclient/app-directive.js
index eee0d3842f..75283598ab 100644
--- a/webclient/app-directive.js
+++ b/webclient/app-directive.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/app-filter.js b/webclient/app-filter.js
index e0e8130e45..27f435674f 100644
--- a/webclient/app-filter.js
+++ b/webclient/app-filter.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
@@ -97,23 +97,28 @@ angular.module('matrixWebClient')
             // Else, build the name from its users
             var room = $rootScope.events.rooms[room_id];
             if (room) {
-                if (room.members) {
+                var room_name_event = room["m.room.name"];
+
+                if (room_name_event) {
+                    roomName = room_name_event.content.name;
+                }
+                else if (room.members) {
                     // Limit the room renaming to 1:1 room
                     if (2 === Object.keys(room.members).length) {
                         for (var i in room.members) {
                             var member = room.members[i];
-                            if (member.user_id !== matrixService.config().user_id) {
+                            if (member.state_key !== matrixService.config().user_id) {
 
-                                if (member.user_id in $rootScope.presence) {
+                                if (member.state_key in $rootScope.presence) {
                                     // If the user is available in presence, use the displayname there
                                     // as it is the most uptodate
-                                    roomName = $rootScope.presence[member.user_id].content.displayname;
+                                    roomName = $rootScope.presence[member.state_key].content.displayname;
                                 }
                                 else if (member.content.displayname) {
                                     roomName = member.content.displayname;
                                 }
                                 else {
-                                    roomName = member.user_id;
+                                    roomName = member.state_key;
                                 }
                             }
                         }
@@ -140,7 +145,7 @@ angular.module('matrixWebClient')
                                 roomName = $rootScope.presence[userID].content.displayname;
                             }
                             else {
-                                roomName = member.user_id;
+                                roomName = userID;
                             }
                         }
                     }
diff --git a/webclient/app.js b/webclient/app.js
index dac4f048cd..d25e2a6234 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/components/fileInput/file-input-directive.js b/webclient/components/fileInput/file-input-directive.js
index c5e4ae07a8..14e2f772f7 100644
--- a/webclient/components/fileInput/file-input-directive.js
+++ b/webclient/components/fileInput/file-input-directive.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js
index 699a3cbffc..e0f67b2c6c 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/webclient/components/fileUpload/file-upload-service.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index d6a0600132..ee478d2eb0 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -32,7 +32,9 @@ angular.module('eventHandlerService', [])
     var MSG_EVENT = "MSG_EVENT";
     var MEMBER_EVENT = "MEMBER_EVENT";
     var PRESENCE_EVENT = "PRESENCE_EVENT";
+    var POWERLEVEL_EVENT = "POWERLEVEL_EVENT";
     var CALL_EVENT = "CALL_EVENT";
+    var NAME_EVENT = "NAME_EVENT";
 
     var InitialSyncDeferred = $q.defer();
     
@@ -95,7 +97,7 @@ angular.module('eventHandlerService', [])
             }
         }
         
-        $rootScope.events.rooms[event.room_id].members[event.user_id] = event;
+        $rootScope.events.rooms[event.room_id].members[event.state_key] = event;
         $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
     };
     
@@ -107,10 +109,20 @@ angular.module('eventHandlerService', [])
     var handlePowerLevels = function(event, isLiveEvent) {
         initRoom(event.room_id);
 
-       $rootScope.events.rooms[event.room_id][event.type] = event;
+        // Keep the latest data. Do not care of events that come when paginating back
+        if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) {
+            $rootScope.events.rooms[event.room_id][event.type] = event;
+            $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent);   
+        }
+    };
 
-        //TODO
-        //$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
+    var handleRoomName = function(event, isLiveEvent) {
+        console.log("handleRoomName " + isLiveEvent);
+
+        initRoom(event.room_id);
+
+        $rootScope.events.rooms[event.room_id][event.type] = event;
+        $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent);
     };
 
     var handleCallEvent = function(event, isLiveEvent) {
@@ -122,7 +134,9 @@ angular.module('eventHandlerService', [])
         MSG_EVENT: MSG_EVENT,
         MEMBER_EVENT: MEMBER_EVENT,
         PRESENCE_EVENT: PRESENCE_EVENT,
+        POWERLEVEL_EVENT: POWERLEVEL_EVENT,
         CALL_EVENT: CALL_EVENT,
+        NAME_EVENT: NAME_EVENT,
         
     
         handleEvent: function(event, isLiveEvent) {
@@ -146,7 +160,9 @@ angular.module('eventHandlerService', [])
                 case 'm.room.power_levels':
                     handlePowerLevels(event, isLiveEvent);
                     break;
-
+                case 'm.room.name':
+                    handleRoomName(event, isLiveEvent);
+                    break;
                 default:
                     console.log("Unable to handle event type " + event.type);
                     console.log(JSON.stringify(event, undefined, 4));
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 441148670e..1c0f7712b4 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js
index 47b63d7f2f..3e13e4e81f 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/webclient/components/matrix/matrix-call.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js
index d9e2e8baa3..ca86b473e7 100644
--- a/webclient/components/matrix/matrix-phone-service.js
+++ b/webclient/components/matrix/matrix-phone-service.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index d399aa3cb9..7c6d4ae50f 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -167,6 +167,29 @@ angular.module('matrixService', [])
             return doRequest("POST", path, undefined, data);
         },
 
+        // Change the membership of an another user
+        setMembership: function(room_id, user_id, membershipValue) {
+            // The REST path spec
+            var path = "/rooms/$room_id/state/m.room.member/$user_id";
+            path = path.replace("$room_id", encodeURIComponent(room_id));
+            path = path.replace("$user_id", user_id);
+
+            return doRequest("PUT", path, undefined, {
+                membership: membershipValue
+            });
+        },
+           
+        // Bans a user from from a room
+        ban: function(room_id, user_id, reason) {
+            var path = "/rooms/$room_id/ban";
+            path = path.replace("$room_id", encodeURIComponent(room_id));
+            
+            return doRequest("POST", path, undefined, {
+                user_id: user_id,
+                reason: reason
+            });
+        },
+
         // Retrieves the room ID corresponding to a room alias
         resolveRoomAlias:function(room_alias) {
             var path = "/_matrix/client/api/v1/directory/room/$room_alias";
@@ -253,7 +276,7 @@ angular.module('matrixService', [])
 
         // get a list of public rooms on your home server
         publicRooms: function() {
-            var path = "/publicRooms"
+            var path = "/publicRooms";
             return doRequest("GET", path);
         },
         
@@ -309,7 +332,7 @@ angular.module('matrixService', [])
 
         // hit the Identity Server for a 3PID request.
         linkEmail: function(email, clientSecret, sendAttempt) {
-            var path = "/_matrix/identity/api/v1/validate/email/requestToken"
+            var path = "/_matrix/identity/api/v1/validate/email/requestToken";
             var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
             var headers = {};
             headers["Content-Type"] = "application/x-www-form-urlencoded";
@@ -414,7 +437,8 @@ angular.module('matrixService', [])
                 state: presence
             });
         },
-
+        
+        
         /****** Permanent storage of user information ******/
         
         // Returns the current config
@@ -514,6 +538,35 @@ angular.module('matrixService', [])
                 }
             }
             return powerLevel;
+        },
+            
+        /**
+         * Change or reset the power level of a user
+         * @param {String} room_id the room id
+         * @param {String} user_id the user id
+         * @param {Number} powerLevel a value between 0 and 10
+         *    If undefined, the user power level will be reset, ie he will use the default room user power level
+         * @returns {promise} an $http promise
+         */
+        setUserPowerLevel: function(room_id, user_id, powerLevel) {
+            
+            // Hack: currently, there is no home server API so do it by hand by updating
+            // the current m.room.power_levels of the room and send it to the server
+            var room = $rootScope.events.rooms[room_id];
+            if (room && room["m.room.power_levels"]) {
+                var content = angular.copy(room["m.room.power_levels"].content);
+                content[user_id] = powerLevel;
+                
+                var path = "/rooms/$room_id/state/m.room.power_levels";
+                path = path.replace("$room_id", encodeURIComponent(room_id));
+                
+                return doRequest("PUT", path, undefined, content);
+            }
+            
+            // The room does not exist or does not contain power_levels data
+            var deferred = $q.defer();
+            deferred.reject({data:{error: "Invalid room: " + room_id}});
+            return deferred.promise;
         }
 
     };
diff --git a/webclient/components/matrix/presence-service.js b/webclient/components/matrix/presence-service.js
index 555118133b..952c8ec8a9 100644
--- a/webclient/components/matrix/presence-service.js
+++ b/webclient/components/matrix/presence-service.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/components/utilities/utilities-service.js b/webclient/components/utilities/utilities-service.js
index 3df2f04458..b417cc5b39 100644
--- a/webclient/components/utilities/utilities-service.js
+++ b/webclient/components/utilities/utilities-service.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
index f4ce3053ea..11b3682d34 100644
--- a/webclient/home/home-controller.js
+++ b/webclient/home/home-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js
index e367b2f0c5..5ef39a7122 100644
--- a/webclient/login/login-controller.js
+++ b/webclient/login/login-controller.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js
index d70e83c3bd..b7584a7d33 100644
--- a/webclient/login/register-controller.js
+++ b/webclient/login/register-controller.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js
index d7d3bf4053..3209f2cbdf 100644
--- a/webclient/recents/recents-controller.js
+++ b/webclient/recents/recents-controller.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/recents/recents-filter.js b/webclient/recents/recents-filter.js
index 45653fca96..d80de6fbeb 100644
--- a/webclient/recents/recents-filter.js
+++ b/webclient/recents/recents-filter.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index db3b0fb32f..9978e08b13 100644
--- a/webclient/recents/recents.html
+++ b/webclient/recents/recents.html
@@ -23,8 +23,8 @@
                     <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" >
                          <div ng-switch-when="m.room.member">
                             {{ room.lastMsg.user_id }}
-                            {{ {"join": "joined", "leave": "left", "invite": "invited"}[room.lastMsg.content.membership] }}
-                            {{ room.lastMsg.content.membership === "invite" ? (room.lastMsg.state_key || '') : '' }}
+                            {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }}
+                            {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }}
                         </div>
 
                         <div ng-switch-when="m.room.message">
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 72c290ad73..52c57856ee 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -85,6 +85,14 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
             updatePresence(event);
         }
     });
+    
+    $scope.$on(eventHandlerService.POWERLEVEL_EVENT, function(ngEvent, event, isLive) {
+        if (isLive && event.room_id === $scope.room_id) {
+            for (var user_id in event.content) {
+                updateUserPowerLevel(user_id);
+            }
+        }
+    });
 
     $scope.memberCount = function() {
         return Object.keys($scope.members).length;
@@ -161,6 +169,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
     var updateMemberList = function(chunk) {
         if (chunk.room_id != $scope.room_id) return;
 
+        // Ignore banned people
+        if ("ban" === chunk.membership) {
+            return;
+        }
+
         // set target_user_id to keep things clear
         var target_user_id = chunk.state_key;
 
@@ -240,6 +253,29 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
         var member = $scope.members[user_id];
         if (member) {
             member.powerLevel = matrixService.getUserPowerLevel($scope.room_id, user_id);
+            
+            normaliseMembersPowerLevels();
+        }
+    }
+
+    // Normalise users power levels so that the user with the higher power level
+    // will have a bar covering 100% of the width of his avatar
+    var normaliseMembersPowerLevels = function() {
+        // Find the max power level
+        var maxPowerLevel = 0;
+        for (var i in $scope.members) {
+            var member = $scope.members[i];
+            if (member.powerLevel) {
+                maxPowerLevel = Math.max(maxPowerLevel, member.powerLevel);
+            }
+        }
+
+        // Normalized them on a 0..100% scale to be use in css width
+        if (maxPowerLevel) {
+            for (var i in $scope.members) {
+                var member = $scope.members[i];
+                member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel;
+            }
         }
     }
 
@@ -250,28 +286,93 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
 
         $scope.state.sending = true;
         
-        // Send the text message
         var promise;
-        // FIXME: handle other commands too
-        if ($scope.textInput.indexOf("/me") === 0) {
-            promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4));
-        }
-        else if ($scope.textInput.indexOf("/nick ") === 0) {
-            // Change user display name
-            promise = matrixService.setDisplayName($scope.textInput.substr(6));
+        
+        // Check for IRC style commands first
+        if ($scope.textInput.indexOf("/") === 0) {
+            var args = $scope.textInput.split(' ');
+            var cmd = args[0];
+            
+            switch (cmd) {
+                case "/me":
+                    var emoteMsg = args.slice(1).join(' ');
+                    promise = matrixService.sendEmoteMessage($scope.room_id, emoteMsg);
+                    break;
+                    
+                case "/nick":
+                    // Change user display name
+                    if (2 === args.length) {
+                        promise = matrixService.setDisplayName(args[1]);
+                    }
+                    break;
+                    
+                case "/kick":
+                    // Kick a user from the room
+                    if (2 === args.length) {
+                        var user_id = args[1];
+
+                        // Set his state in the room as leave
+                        promise = matrixService.setMembership($scope.room_id, user_id, "leave");
+                    }
+                    break;
+                    
+                case "/ban":
+                    // Ban a user from the room
+                    if (2 <= args.length) {
+                        // TODO: The user may have entered the display name
+                        // Need display name -> user_id resolution. Pb: how to manage user with same display names?
+                        var user_id = args[1];
+
+                        // Does the user provide a reason?
+                        if (3 <= args.length) {
+                            var reason = args.slice(2).join(' ');
+                        }
+                        promise = matrixService.ban($scope.room_id, user_id, reason);
+                    }
+                    break;
+                    
+                case "/unban":
+                    // Unban a user from the room
+                    if (2 === args.length) {
+                        var user_id = args[1];
+
+                        // Reset the user membership to leave to unban him
+                        promise = matrixService.setMembership($scope.room_id, user_id, "leave");
+                    }
+                    break;
+                    
+                case "/op":
+                    // Define the power level of a user
+                    if (3 === args.length) {
+                        var user_id = args[1];
+                        var powerLevel = parseInt(args[2]);
+                        promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel);
+                    }
+                    break;
+                    
+                case "/deop":
+                    // Reset the power level of a user
+                    if (2 === args.length) {
+                        var user_id = args[1];
+                        promise = matrixService.setUserPowerLevel($scope.room_id, user_id, undefined);
+                    }
+                    break;
+            }
         }
-        else {
+        
+        if (!promise) {
+            // Send the text message
             promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput);
         }
         
         promise.then(
             function() {
-                console.log("Sent message");
+                console.log("Request successfully sent");
                 $scope.textInput = "";
                 $scope.state.sending = false;
             },
             function(error) {
-                $scope.feedback = "Failed to send: " + error.data.error;
+                $scope.feedback = "Request failed: " + error.data.error;
                 $scope.state.sending = false;
             });
     };
@@ -363,7 +464,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
                             onInit3();
                         },
                         function(reason) {
-                            $scope.feedback = "Can't join room: " + reason;
+                            console.log("Can't join room: " + JSON.stringify(reason));
+                            $scope.feedback = "You do not have permission to join this room";
                         });
                 }
                 else {
diff --git a/webclient/room/room-directive.js b/webclient/room/room-directive.js
index 1a99a37abb..659bcbc60f 100644
--- a/webclient/room/room-directive.js
+++ b/webclient/room/room-directive.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/room/room.html b/webclient/room/room.html
index e672b1d7e2..e29f511ecf 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -24,7 +24,7 @@
                          title="{{ member.id }}"
                          width="80" height="80"/>
                     <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
-                    <div class="userPowerLevel" ng-style="{'width': (10 * member.powerLevel) +'%'}"></div>
+                    <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div>
                     <div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div>
                 </td>
                 <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
@@ -50,8 +50,9 @@
                     <div class="bubble">
                         <span ng-show='msg.type === "m.room.member"'>
                             {{ members[msg.user_id].displayname || msg.user_id }}
-                            {{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
-                            {{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }}
+                            {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }}
+                            {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }}
+
                         </span>
                         <span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
                         <span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js
index dc680ef075..7a26367a1b 100644
--- a/webclient/settings/settings-controller.js
+++ b/webclient/settings/settings-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html
index 03927838d2..49dc603540 100644
--- a/webclient/settings/settings.html
+++ b/webclient/settings/settings.html
@@ -74,6 +74,21 @@
             <div>Access token: {{ config.access_token }} </div>
         </div>
         <br/>
+        
+        <h3>Commands</h3>
+        <div class="section">
+            The following commands are available in the room chat:
+            <ul>
+                <li>/nick &lt;display_name&gt;: change your display name</li>
+                <li>/me &lt;action&gt;: send the action you are doing. /me will be replaced by your display name</li>
+                <li>/kick &lt;user_id&gt;: kick the user</li>
+                <li>/ban &lt;user_id&gt; [&lt;reason&gt;]: ban the user</li>
+                <li>/unban &lt;user_id&gt;: unban the user</li>
+                <li>/op &lt;user_id&gt; &lt;power_level&gt;: set user power level</li>
+                <li>/deop &lt;user_id&gt;: reset user power level to the room default value</li>
+            </ul>
+        </div>
+        <br/>
 
         {{ feedback }}
 
diff --git a/webclient/user/user-controller.js b/webclient/user/user-controller.js
index b5b2d439a2..3940db6683 100644
--- a/webclient/user/user-controller.js
+++ b/webclient/user/user-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.