summary refs log tree commit diff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--CHANGES.rst12
-rw-r--r--README.rst6
-rw-r--r--docs/client-server/swagger_matrix/api-docs38
-rw-r--r--docs/client-server/swagger_matrix/events299
-rw-r--r--docs/client-server/swagger_matrix/login102
-rw-r--r--docs/client-server/swagger_matrix/presence164
-rw-r--r--docs/client-server/swagger_matrix/profile122
-rw-r--r--docs/client-server/swagger_matrix/registration75
-rw-r--r--docs/client-server/swagger_matrix/rooms807
-rwxr-xr-xsynapse/app/homeserver.py7
-rw-r--r--synapse/handlers/presence.py8
-rw-r--r--synapse/handlers/room.py2
-rw-r--r--synapse/http/server.py4
-rw-r--r--synapse/rest/register.py4
-rw-r--r--synapse/storage/roommember.py2
-rw-r--r--synapse/storage/schema/im.sql23
-rw-r--r--tests/handlers/test_presence.py7
-rw-r--r--tests/handlers/test_presencelike.py4
-rw-r--r--tests/rest/test_presence.py2
-rw-r--r--webclient/app.css98
-rw-r--r--webclient/index.html6
-rw-r--r--webclient/login/login.html4
-rw-r--r--webclient/room/room.html30
-rw-r--r--webclient/user/user.html1
24 files changed, 1787 insertions, 40 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 055f8bc01b..fc6385cb1f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,8 @@
-Changes in synapse 0.0.1
-=======================
+Changes in synapse 0.0.1 (2014-08-22)
+=====================================
+Presence has been disabled in this release due to a bug that caused the
+homeserver to spam other remote homeservers.
+
 Homeserver:
  * Completely change the database schema to support generic event types.
  * Improve presence reliability.
@@ -18,3 +21,8 @@ Webclient:
  * Use the new initial sync API to reduce number of round trips to the homeserver.
  * Change url scheme to use room aliases instead of room ids where known.
  * Increase longpoll timeout.
+
+Changes in synapse 0.0.0 (2014-08-13)
+=====================================
+
+ * Initial alpha release
diff --git a/README.rst b/README.rst
index 069f37ec06..cfdc2a1c75 100644
--- a/README.rst
+++ b/README.rst
@@ -34,6 +34,10 @@ To get up and running:
       machine.my.domain.name``.  Then come join ``#matrix:matrix.org`` and
       say hi! :)
 
+For more detailed setup instructions, please see further down this document.
+
+[1] VoIP currently in development
+
    
 About Matrix
 ============
@@ -85,8 +89,6 @@ https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
 
 Thanks for trying Matrix!
 
-[1] VoIP currently in development
-
 [2] Cryptographic signing of messages isn't turned on yet
 
 [3] End-to-end encryption is currently in development
diff --git a/docs/client-server/swagger_matrix/api-docs b/docs/client-server/swagger_matrix/api-docs
new file mode 100644
index 0000000000..d974dbb374
--- /dev/null
+++ b/docs/client-server/swagger_matrix/api-docs
@@ -0,0 +1,38 @@
+{
+  "apiVersion": "1.0.0",
+  "swaggerVersion": "1.2",
+  "apis": [
+    {
+      "path": "/login",
+      "description": "Login operations"
+    },
+    {
+      "path": "/registration",
+      "description": "Registration operations"
+    },
+    {
+      "path": "/rooms",
+      "description": "Room operations"
+    },
+    {
+      "path": "/profile",
+      "description": "Profile operations"
+    },
+    {
+      "path": "/presence",
+      "description": "Presence operations"
+    }
+  ],
+  "authorizations": {
+    "token": {
+      "scopes": []
+    }
+  },
+  "info": {
+    "title": "Matrix Client-Server API Reference",
+    "description": "This contains the client-server API for the reference implementation of the home server",
+    "termsOfServiceUrl": "http://matrix.org",
+    "license": "Apache 2.0",
+    "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html"
+  }
+}
diff --git a/docs/client-server/swagger_matrix/events b/docs/client-server/swagger_matrix/events
new file mode 100644
index 0000000000..c9eb3f6ff7
--- /dev/null
+++ b/docs/client-server/swagger_matrix/events
@@ -0,0 +1,299 @@
+{
+  "apiVersion": "1.0.0",
+  "swaggerVersion": "1.2",
+  "basePath": "http://petstore.swagger.wordnik.com/api",
+  "resourcePath": "/user",
+  "produces": [
+    "application/json"
+  ],
+  "apis": [
+    {
+      "path": "/user",
+      "operations": [
+        {
+          "method": "POST",
+          "summary": "Create user",
+          "notes": "This can only be done by the logged in user.",
+          "type": "void",
+          "nickname": "createUser",
+          "authorizations": {
+            "oauth2": [
+              {
+                "scope": "test:anything",
+                "description": "anything"
+              }
+            ]
+          },
+          "parameters": [
+            {
+              "name": "body",
+              "description": "Created user object",
+              "required": true,
+              "type": "User",
+              "paramType": "body"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/user/logout",
+      "operations": [
+        {
+          "method": "GET",
+          "summary": "Logs out current logged in user session",
+          "notes": "",
+          "type": "void",
+          "nickname": "logoutUser",
+          "authorizations": {},
+          "parameters": []
+        }
+      ]
+    },
+    {
+      "path": "/user/createWithArray",
+      "operations": [
+        {
+          "method": "POST",
+          "summary": "Creates list of users with given input array",
+          "notes": "",
+          "type": "void",
+          "nickname": "createUsersWithArrayInput",
+          "authorizations": {
+            "oauth2": [
+              {
+                "scope": "test:anything",
+                "description": "anything"
+              }
+            ]
+          },
+          "parameters": [
+            {
+              "name": "body",
+              "description": "List of user object",
+              "required": true,
+              "type": "array",
+              "items": {
+                "$ref": "User"
+              },
+              "paramType": "body"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/user/createWithList",
+      "operations": [
+        {
+          "method": "POST",
+          "summary": "Creates list of users with given list input",
+          "notes": "",
+          "type": "void",
+          "nickname": "createUsersWithListInput",
+          "authorizations": {
+            "oauth2": [
+              {
+                "scope": "test:anything",
+                "description": "anything"
+              }
+            ]
+          },
+          "parameters": [
+            {
+              "name": "body",
+              "description": "List of user object",
+              "required": true,
+              "type": "array",
+              "items": {
+                "$ref": "User"
+              },
+              "paramType": "body"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/user/{username}",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Updated user",
+          "notes": "This can only be done by the logged in user.",
+          "type": "void",
+          "nickname": "updateUser",
+          "authorizations": {
+            "oauth2": [
+              {
+                "scope": "test:anything",
+                "description": "anything"
+              }
+            ]
+          },
+          "parameters": [
+            {
+              "name": "username",
+              "description": "name that need to be deleted",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "body",
+              "description": "Updated user object",
+              "required": true,
+              "type": "User",
+              "paramType": "body"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 400,
+              "message": "Invalid username supplied"
+            },
+            {
+              "code": 404,
+              "message": "User not found"
+            }
+          ]
+        },
+        {
+          "method": "DELETE",
+          "summary": "Delete user",
+          "notes": "This can only be done by the logged in user.",
+          "type": "void",
+          "nickname": "deleteUser",
+          "authorizations": {
+            "oauth2": [
+              {
+                "scope": "test:anything",
+                "description": "anything"
+              }
+            ]
+          },
+          "parameters": [
+            {
+              "name": "username",
+              "description": "The name that needs to be deleted",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 400,
+              "message": "Invalid username supplied"
+            },
+            {
+              "code": 404,
+              "message": "User not found"
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get user by user name",
+          "notes": "",
+          "type": "User",
+          "nickname": "getUserByName",
+          "authorizations": {},
+          "parameters": [
+            {
+              "name": "username",
+              "description": "The name that needs to be fetched. Use user1 for testing.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 400,
+              "message": "Invalid username supplied"
+            },
+            {
+              "code": 404,
+              "message": "User not found"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/user/login",
+      "operations": [
+        {
+          "method": "GET",
+          "summary": "Logs user into the system",
+          "notes": "",
+          "type": "string",
+          "nickname": "loginUser",
+          "authorizations": {},
+          "parameters": [
+            {
+              "name": "username",
+              "description": "The user name for login",
+              "required": true,
+              "type": "string",
+              "paramType": "query"
+            },
+            {
+              "name": "password",
+              "description": "The password for login in clear text",
+              "required": true,
+              "type": "string",
+              "paramType": "query"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 400,
+              "message": "Invalid username and password combination"
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "models": {
+    "User": {
+      "id": "User",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "firstName": {
+          "type": "string"
+        },
+        "username": {
+          "type": "string"
+        },
+        "lastName": {
+          "type": "string"
+        },
+        "email": {
+          "type": "string"
+        },
+        "password": {
+          "type": "string"
+        },
+        "phone": {
+          "type": "string"
+        },
+        "userStatus": {
+          "type": "integer",
+          "format": "int32",
+          "description": "User Status",
+          "enum": [
+            "1-registered",
+            "2-active",
+            "3-closed"
+          ]
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/docs/client-server/swagger_matrix/login b/docs/client-server/swagger_matrix/login
new file mode 100644
index 0000000000..4410d3c887
--- /dev/null
+++ b/docs/client-server/swagger_matrix/login
@@ -0,0 +1,102 @@
+{
+  "apiVersion": "1.0.0", 
+  "apis": [
+    {
+      "operations": [
+        {
+          "method": "GET", 
+          "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"
+        }, 
+        {
+          "method": "POST", 
+          "nickname": "submit_login", 
+          "notes": "If this is part of a multi-stage login, there MUST be a 'session' key.", 
+          "parameters": [
+            {
+              "description": "A login submission", 
+              "name": "body", 
+              "paramType": "body", 
+              "required": true, 
+              "type": "LoginSubmission"
+            }
+          ], 
+          "responseMessages": [
+            {
+              "code": 400, 
+              "message": "Bad login type"
+            }, 
+            {
+              "code": 400, 
+              "message": "Missing JSON keys"
+            }
+          ], 
+          "summary": "Submit a login action.", 
+          "type": "LoginResult"
+        }
+      ], 
+      "path": "/login"
+    }
+  ], 
+  "basePath": "http://localhost:8080/matrix/client/api/v1", 
+  "consumes": [
+    "application/json"
+  ], 
+  "models": {
+    "LoginInfo": {
+      "id": "LoginInfo", 
+      "properties": {
+        "stages": {
+          "description": "Multi-stage login only: An array of all the login types required to login.", 
+          "format": "string", 
+          "type": "array"
+        }, 
+        "type": {
+          "description": "The login type that must be used when logging in.", 
+          "type": "string"
+        }
+      }
+    }, 
+    "LoginResult": {
+      "id": "LoginResult", 
+      "properties": {
+        "access_token": {
+          "description": "The access token for this user's login if this is the final stage of the login process.", 
+          "type": "string"
+        }, 
+        "next": {
+          "description": "Multi-stage login only: The next login type to submit.", 
+          "type": "string"
+        },
+        "session": {
+          "description": "Multi-stage login only: The session token to send when submitting the next login type.",
+          "type": "string"
+        }
+      }
+    }, 
+    "LoginSubmission": {
+      "id": "LoginSubmission", 
+      "properties": {
+        "type": {
+          "description": "The type of login being submitted.", 
+          "type": "string"
+        },
+        "session": {
+          "description": "Multi-stage login only: The session token from an earlier login stage.",
+          "type": "string"
+        },
+        "_login_type_defined_keys_": {
+          "description": "Keys as defined by the specified login type, e.g. \"user\", \"password\""
+        }
+      }
+    }
+  }, 
+  "produces": [
+    "application/json"
+  ], 
+  "resourcePath": "/login", 
+  "swaggerVersion": "1.2"
+}
+
diff --git a/docs/client-server/swagger_matrix/presence b/docs/client-server/swagger_matrix/presence
new file mode 100644
index 0000000000..ee9deb12f0
--- /dev/null
+++ b/docs/client-server/swagger_matrix/presence
@@ -0,0 +1,164 @@
+{
+  "apiVersion": "1.0.0",
+  "swaggerVersion": "1.2",
+  "basePath": "http://localhost:8080/matrix/client/api/v1",
+  "resourcePath": "/presence",
+  "produces": [
+    "application/json"
+  ],
+  "consumes": [
+    "application/json"
+  ],
+  "apis": [
+    {
+      "path": "/presence/{userId}/status",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Update this user's presence state.",
+          "notes": "This can only be done by the logged in user.",
+          "type": "void",
+          "nickname": "update_presence",
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The new presence state",
+              "required": true,
+              "type": "PresenceUpdate",
+              "paramType": "body"
+            },
+            {
+              "name": "userId",
+              "description": "The user whose presence to set.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get this user's presence state.",
+          "notes": "Get this user's presence state.",
+          "type": "PresenceUpdate",
+          "nickname": "get_presence",
+          "parameters": [
+            {
+              "name": "userId",
+              "description": "The user whose presence to get.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/presence_list/{userId}",
+      "operations": [
+        {
+          "method": "GET",
+          "summary": "Retrieve a list of presences for all of this user's friends.",
+          "notes": "",
+          "type": "array",
+          "items": {
+            "$ref": "Presence"
+          },
+          "nickname": "get_presence_list",
+          "parameters": [
+            {
+              "name": "userId",
+              "description": "The user whose presence list to get.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ]
+        },
+        {
+          "method": "POST",
+          "summary": "Add or remove users from this presence list.",
+          "notes": "Add or remove users from this presence list.",
+          "type": "void",
+          "nickname": "modify_presence_list",
+          "parameters": [
+            {
+              "name": "userId",
+              "description": "The user whose presence list is being modified.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "body",
+              "description": "The modifications to make to this presence list.",
+              "required": true,
+              "type": "PresenceListModifications",
+              "paramType": "body"
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "models": {
+    "PresenceUpdate": {
+      "id": "PresenceUpdate",
+      "properties": {
+        "state": {
+          "type": "string",
+          "description": "Enum: The presence state.",
+          "enum": [
+            "offline",
+            "unavailable",
+            "online",
+            "free_for_chat"
+          ]
+        },
+        "status_msg": {
+          "type": "string",
+          "description": "The user-defined message associated with this presence state."
+        }
+      },
+      "subTypes": [
+        "Presence"
+      ]
+    },
+    "Presence": {
+      "id": "Presence",
+      "properties": {
+        "mtime_age": {
+          "type": "integer",
+          "format": "int64",
+          "description": "The last time this user's presence state changed, in milliseconds."
+        },
+        "user_id": {
+          "type": "string",
+          "description": "The fully qualified user ID"
+        }
+      }
+    },
+    "PresenceListModifications": {
+      "id": "PresenceListModifications",
+      "properties": {
+        "invite": {
+          "type": "array",
+          "description": "A list of user IDs to add to the list.",
+          "items": {
+            "type": "string",
+            "description": "A fully qualified user ID."
+          }
+        },
+        "drop": {
+          "type": "array",
+          "description": "A list of user IDs to remove from the list.",
+          "items": {
+            "type": "string",
+            "description": "A fully qualified user ID."
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/docs/client-server/swagger_matrix/profile b/docs/client-server/swagger_matrix/profile
new file mode 100644
index 0000000000..1ebde62e20
--- /dev/null
+++ b/docs/client-server/swagger_matrix/profile
@@ -0,0 +1,122 @@
+{
+  "apiVersion": "1.0.0",
+  "swaggerVersion": "1.2",
+  "basePath": "http://localhost:8080/matrix/client/api/v1",
+  "resourcePath": "/profile",
+  "produces": [
+    "application/json"
+  ],
+  "consumes": [
+    "application/json"
+  ],
+  "apis": [
+    {
+      "path": "/profile/{userId}/displayname",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Set a display name.",
+          "notes": "This can only be done by the logged in user.",
+          "type": "void",
+          "nickname": "set_display_name",
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The new display name for this user.",
+              "required": true,
+              "type": "DisplayName",
+              "paramType": "body"
+            },
+            {
+              "name": "userId",
+              "description": "The user whose display name to set.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get a display name.",
+          "notes": "This can be done by anyone.",
+          "type": "DisplayName",
+          "nickname": "get_display_name",
+          "parameters": [
+            {
+              "name": "userId",
+              "description": "The user whose display name to get.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/profile/{userId}/avatar_url",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Set an avatar URL.",
+          "notes": "This can only be done by the logged in user.",
+          "type": "void",
+          "nickname": "set_avatar_url",
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The new avatar url for this user.",
+              "required": true,
+              "type": "AvatarUrl",
+              "paramType": "body"
+            },
+            {
+              "name": "userId",
+              "description": "The user whose avatar url to set.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get an avatar url.",
+          "notes": "This can be done by anyone.",
+          "type": "AvatarUrl",
+          "nickname": "get_avatar_url",
+          "parameters": [
+            {
+              "name": "userId",
+              "description": "The user whose avatar url to get.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "models": {
+    "DisplayName": {
+      "id": "DisplayName",
+      "properties": {
+        "displayname": {
+          "type": "string",
+          "description": "The textual display name"
+        }
+      }
+    },
+    "AvatarUrl": {
+      "id": "AvatarUrl",
+      "properties": {
+        "avatar_url": {
+          "type": "string",
+          "description": "A url to an image representing an avatar."
+        }
+      }
+    }
+  }
+}
diff --git a/docs/client-server/swagger_matrix/registration b/docs/client-server/swagger_matrix/registration
new file mode 100644
index 0000000000..ccd542d11e
--- /dev/null
+++ b/docs/client-server/swagger_matrix/registration
@@ -0,0 +1,75 @@
+{
+  "apiVersion": "1.0.0", 
+  "apis": [
+    {
+      "operations": [
+        {
+          "method": "POST", 
+          "nickname": "register", 
+          "notes": "Volatile: This API is likely to change.", 
+          "parameters": [
+            {
+              "description": "A registration request", 
+              "name": "body", 
+              "paramType": "body", 
+              "required": true, 
+              "type": "RegistrationRequest"
+            }
+          ], 
+          "responseMessages": [
+            {
+              "code": 400, 
+              "message": "No JSON object."
+            }, 
+            {
+              "code": 400, 
+              "message": "User ID must only contain characters which do not require url encoding."
+            },
+            {
+              "code": 400, 
+              "message": "User ID already taken."
+            }
+          ], 
+          "summary": "Register with the home server.", 
+          "type": "RegistrationResponse"
+        }
+      ], 
+      "path": "/register"
+    }
+  ], 
+  "basePath": "http://localhost:8080/matrix/client/api/v1", 
+  "consumes": [
+    "application/json"
+  ], 
+  "models": {
+    "RegistrationResponse": {
+      "id": "RegistrationResponse", 
+      "properties": {
+        "access_token": {
+          "description": "The access token for this user.", 
+          "type": "string"
+        }, 
+        "user_id": {
+          "description": "The fully-qualified user ID.", 
+          "type": "string"
+        }
+      }
+    }, 
+    "RegistrationRequest": {
+      "id": "RegistrationRequest", 
+      "properties": {
+        "user_id": {
+          "description": "The desired user ID. If not specified, a random user ID will be allocated.", 
+          "type": "string",
+          "required": false
+        }
+      }
+    }
+  }, 
+  "produces": [
+    "application/json"
+  ], 
+  "resourcePath": "/register", 
+  "swaggerVersion": "1.2"
+}
+
diff --git a/docs/client-server/swagger_matrix/rooms b/docs/client-server/swagger_matrix/rooms
new file mode 100644
index 0000000000..47a8887240
--- /dev/null
+++ b/docs/client-server/swagger_matrix/rooms
@@ -0,0 +1,807 @@
+{
+  "apiVersion": "1.0.0",
+  "swaggerVersion": "1.2",
+  "basePath": "http://localhost:8080/matrix/client/api/v1", 
+  "resourcePath": "/rooms",
+  "produces": [
+    "application/json"
+  ],
+  "consumes": [
+    "application/json"
+  ],
+  "authorizations": {
+    "token": []
+  },
+  "apis": [
+    {
+      "path": "/rooms/{roomId}/messages/{userId}/{messageId}",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Send a message in this room.",
+          "notes": "Send a message in this room.",
+          "type": "void",
+          "nickname": "send_message",
+          "consumes": [
+            "application/json"
+          ],
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The message contents",
+              "required": true,
+              "type": "Message",
+              "paramType": "body"
+            },
+            {
+              "name": "roomId",
+              "description": "The room to send the message in.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "userId",
+              "description": "The fully qualified message sender's user ID.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "messageId",
+              "description": "A message ID which is unique for each room and user.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 403,
+              "message": "Must send messages as yourself."
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get a message from this room.",
+          "notes": "Get a message from this room.",
+          "type": "Message",
+          "nickname": "get_message",
+          "parameters": [
+            {
+              "name": "roomId",
+              "description": "The room to send the message in.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "userId",
+              "description": "The fully qualified message sender's user ID.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "messageId",
+              "description": "A message ID which is unique for each room and user.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 404,
+              "message": "Message not found."
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/rooms/{roomId}/topic",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Set the topic for this room.",
+          "notes": "Set the topic for this room.",
+          "type": "void",
+          "nickname": "set_topic",
+          "consumes": [
+            "application/json"
+          ],
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The topic contents",
+              "required": true,
+              "type": "Topic",
+              "paramType": "body"
+            },
+            {
+              "name": "roomId",
+              "description": "The room to set the topic in.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 403,
+              "message": "Must send messages as yourself."
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get the topic for this room.",
+          "notes": "Get the topic for this room.",
+          "type": "Topic",
+          "nickname": "get_topic",
+          "parameters": [
+            {
+              "name": "roomId",
+              "description": "The room to get topic in.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 404,
+              "message": "Topic not found."
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/rooms/{roomId}/messages/{msgSenderId}/{messageId}/feedback/{senderId}/{feedbackType}",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Send feedback to a message.",
+          "notes": "Send feedback to a message.",
+          "type": "void",
+          "nickname": "send_feedback",
+          "consumes": [
+            "application/json"
+          ],
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The feedback contents",
+              "required": true,
+              "type": "Feedback",
+              "paramType": "body"
+            },
+            {
+              "name": "roomId",
+              "description": "The room to send the feedback in.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "msgSenderId",
+              "description": "The fully qualified message sender's user ID.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "messageId",
+              "description": "A message ID which is unique for each room and user.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "senderId",
+              "description": "The fully qualified feedback sender's user ID.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "feedbackType",
+              "description": "The type of feedback being sent.",
+              "required": true,
+              "type": "string",
+              "paramType": "path",
+              "enum": [
+                "d",
+                "r"
+              ]
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 403,
+              "message": "Must send feedback as yourself."
+            },
+            {
+              "code": 400,
+              "message": "Bad feedback type."
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get feedback for a message.",
+          "notes": "Get feedback for a message.",
+          "type": "Feedback",
+          "nickname": "get_feedback",
+          "parameters": [
+            {
+              "name": "roomId",
+              "description": "The room to send the message in.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "msgSenderId",
+              "description": "The fully qualified message sender's user ID.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "messageId",
+              "description": "A message ID which is unique for each room and user.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "senderId",
+              "description": "The fully qualified feedback sender's user ID.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "feedbackType",
+              "description": "Enum: The type of feedback being sent.",
+              "required": true,
+              "type": "string",
+              "paramType": "path",
+              "enum": [
+                "d",
+                "r"
+              ]
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 404,
+              "message": "Feedback not found."
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/rooms/{roomId}/members/{userId}/state",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Change the membership state for a user in a room.",
+          "notes": "Change the membership state for a user in a room.",
+          "type": "void",
+          "nickname": "set_membership",
+          "consumes": [
+            "application/json"
+          ],
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The new membership state",
+              "required": true,
+              "type": "Member",
+              "paramType": "body"
+            },
+            {
+              "name": "userId",
+              "description": "The user whose membership is being changed.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "roomId",
+              "description": "The room which has this user.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 400,
+              "message": "No membership key."
+            },
+            {
+              "code": 400,
+              "message": "Bad membership value."
+            },
+            {
+              "code": 403,
+              "message": "When inviting: You are not in the room."
+            },
+            {
+              "code": 403,
+              "message": "When inviting: <target> is already in the room."
+            },
+            {
+              "code": 403,
+              "message": "When joining: Cannot force another user to join."
+            },
+            {
+              "code": 403,
+              "message": "When joining: You are not invited to this room."
+            }
+          ]
+        },
+        {
+          "method": "GET",
+          "summary": "Get the membership state of a user in a room.",
+          "notes": "Get the membership state of a user in a room.",
+          "type": "Member",
+          "nickname": "get_membership",
+          "parameters": [
+            {
+              "name": "userId",
+              "description": "The user whose membership state you want to get.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "roomId",
+              "description": "The room which has this user.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 404,
+              "message": "Member not found."
+            }
+          ]
+        },
+        {
+          "method": "DELETE",
+          "summary": "Leave a room.",
+          "notes": "Leave a room.",
+          "type": "void",
+          "nickname": "remove_membership",
+          "parameters": [
+            {
+              "name": "userId",
+              "description": "The user who is leaving.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "roomId",
+              "description": "The room which has this user.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 403,
+              "message": "You are not in the room."
+            },
+            {
+              "code": 403,
+              "message": "Cannot force another user to leave."
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/join/{roomAlias}",
+      "operations": [
+        {
+          "method": "PUT",
+          "summary": "Join a room via a room alias.",
+          "notes": "Join a room via a room alias.",
+          "type": "RoomInfo",
+          "nickname": "join_room_via_alias",
+          "consumes": [
+            "application/json"
+          ],
+          "parameters": [
+            {
+              "name": "roomAlias",
+              "description": "The room alias to join.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 400,
+              "message": "Bad room alias."
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/rooms",
+      "operations": [
+        {
+          "method": "POST",
+          "summary": "Create a room.",
+          "notes": "Create a room.",
+          "type": "RoomInfo",
+          "nickname": "create_room",
+          "consumes": [
+            "application/json"
+          ],
+          "parameters": [
+            {
+              "name": "body",
+              "description": "The desired configuration for the room.",
+              "required": true,
+              "type": "RoomConfig",
+              "paramType": "body"
+            }
+          ],
+          "responseMessages": [
+            {
+              "code": 400,
+              "message": "Body must be JSON."
+            },
+            {
+              "code": 400,
+              "message": "Room alias already taken."
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/rooms/{roomId}/messages/list",
+      "operations": [
+        {
+          "method": "GET",
+          "summary": "Get a list of messages for this room.",
+          "notes": "Get a list of messages for this room.",
+          "type": "MessagePaginationChunk",
+          "nickname": "get_messages",
+          "parameters": [
+            {
+              "name": "roomId",
+              "description": "The room to get messages in.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "from",
+              "description": "The token to start getting results from.",
+              "required": false,
+              "type": "string",
+              "paramType": "query"
+            },
+            {
+              "name": "to",
+              "description": "The token to stop getting results at.",
+              "required": false,
+              "type": "string",
+              "paramType": "query"
+            },
+            {
+              "name": "limit",
+              "description": "The maximum number of messages to return.",
+              "required": false,
+              "type": "integer",
+              "paramType": "query"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/rooms/{roomId}/members/list",
+      "operations": [
+        {
+          "method": "GET",
+          "summary": "Get a list of members for this room.",
+          "notes": "Get a list of members for this room.",
+          "type": "MemberPaginationChunk",
+          "nickname": "get_members",
+          "parameters": [
+            {
+              "name": "roomId",
+              "description": "The room to get a list of members from.",
+              "required": true,
+              "type": "string",
+              "paramType": "path"
+            },
+            {
+              "name": "from",
+              "description": "The token to start getting results from.",
+              "required": false,
+              "type": "string",
+              "paramType": "query"
+            },
+            {
+              "name": "to",
+              "description": "The token to stop getting results at.",
+              "required": false,
+              "type": "string",
+              "paramType": "query"
+            },
+            {
+              "name": "limit",
+              "description": "The maximum number of members to return.",
+              "required": false,
+              "type": "integer",
+              "paramType": "query"
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "models": {
+    "Topic": {
+      "id": "Topic",
+      "properties": {
+        "topic": {
+          "type": "string",
+          "description": "The topic text"
+        }
+      }
+    },
+    "Message": {
+      "id": "Message",
+      "properties": {
+        "msgtype": {
+          "type": "string",
+          "description": "The type of message being sent, e.g. \"m.text\"",
+          "required": true
+        },
+        "_msgtype_defined_keys_": {
+          "description": "Additional keys as defined by the msgtype, e.g. \"body\""
+        }
+      }
+    },
+    "Feedback": {
+      "id": "Feedback",
+      "properties": {
+      }
+    },
+    "Member": {
+      "id": "Member",
+      "properties": {
+        "membership": {
+          "type": "string",
+          "description": "Enum: The membership state of this member.",
+          "enum": [
+            "invite",
+            "join",
+            "leave",
+            "knock"
+          ]
+        }
+      }
+    },
+    "RoomInfo": {
+      "id": "RoomInfo",
+      "properties": {
+        "room_id": {
+          "type": "string",
+          "description": "The allocated room ID.",
+          "required": true
+        },
+        "room_alias": {
+          "type": "string",
+          "description": "The alias for the room.",
+          "required": false
+        }
+      }
+    },
+    "RoomConfig": {
+      "id": "RoomConfig",
+      "properties": {
+        "visibility": {
+          "type": "string",
+          "description": "Enum: The room visibility.",
+          "required": false,
+          "enum": [
+            "public",
+            "private"
+          ]
+        },
+        "room_alias_name": {
+          "type": "string",
+          "description": "The alias to give the new room.",
+          "required": false
+        }
+      }
+    },
+    "PaginationRequest": {
+      "id": "PaginationRequest",
+      "properties": {
+        "from": {
+          "type": "string",
+          "description": "The token to start getting results from."
+        },
+        "to": {
+          "type": "string",
+          "description": "The token to stop getting results at."
+        },
+        "limit": {
+          "type": "integer",
+          "description": "The maximum number of entries to return."
+        }
+      }
+    },
+    "PaginationChunk": {
+      "id": "PaginationChunk",
+      "properties": {
+        "start": {
+          "type": "string",
+          "description": "A token which correlates to the first value in \"chunk\" for paginating.",
+          "required": true
+        },
+        "end": {
+          "type": "string",
+          "description": "A token which correlates to the last value in \"chunk\" for paginating.",
+          "required": true
+        }
+      },
+      "subTypes": [
+        "MessagePaginationChunk"
+      ]
+    },
+    "MessagePaginationChunk": {
+      "id": "MessagePaginationChunk",
+      "properties": {
+        "chunk": {
+          "type": "array",
+          "description": "A list of message events.",
+          "items": {
+            "$ref": "MessageEvent"
+          },
+          "required": true
+        }
+      }
+    },
+    "MemberPaginationChunk": {
+      "id": "MemberPaginationChunk",
+      "properties": {
+        "chunk": {
+          "type": "array",
+          "description": "A list of member events.",
+          "items": {
+            "$ref": "MemberEvent"
+          },
+          "required": true
+        }
+      }
+    },
+    "Event": {
+      "id": "Event",
+      "properties": {
+        "event_id": {
+          "type": "string",
+          "description": "An ID which uniquely identifies this event.",
+          "required": true
+        },
+        "room_id": {
+          "type": "string",
+          "description": "The room in which this event occurred.",
+          "required": true
+        }
+      },
+      "subTypes": [
+        "MessageEvent"
+      ]
+    },
+    "MessageEvent": {
+      "id": "MessageEvent",
+      "properties": {
+        "content": {
+          "type": "Message"
+        }
+      }
+    },
+    "MemberEvent": {
+      "id": "MemberEvent",
+      "properties": {
+        "content": {
+          "type": "Member"
+        }
+      }
+    },
+    "Tag": {
+      "id": "Tag",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "name": {
+          "type": "string"
+        }
+      }
+    },
+    "Pet": {
+      "id": "Pet",
+      "required": [
+        "id",
+        "name"
+      ],
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64",
+          "description": "unique identifier for the pet",
+          "minimum": "0.0",
+          "maximum": "100.0"
+        },
+        "category": {
+          "$ref": "Category"
+        },
+        "name": {
+          "type": "string"
+        },
+        "photoUrls": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "tags": {
+          "type": "array",
+          "items": {
+            "$ref": "Tag"
+          }
+        },
+        "status": {
+          "type": "string",
+          "description": "pet status in the store",
+          "enum": [
+            "available",
+            "pending",
+            "sold"
+          ]
+        }
+      }
+    },
+    "Category": {
+      "id": "Category",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "name": {
+          "type": "string"
+        },
+        "pet": {
+          "$ref": "Pet"
+        }
+      }
+    }
+  }
+}
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 495149466c..40e3561ee5 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -37,6 +37,7 @@ import logging
 import logging.config
 import sqlite3
 import os
+import re
 
 logger = logging.getLogger(__name__)
 
@@ -255,8 +256,14 @@ def setup():
 
     logger.info("Server hostname: %s", args.host)
 
+    if re.search(":[0-9]+$", args.host):
+        domain_with_port = args.host
+    else:
+        domain_with_port = "%s:%s" % (args.host, args.port)
+
     hs = SynapseHomeServer(
         args.host,
+        domain_with_port=domain_with_port,
         upload_dir=os.path.abspath("uploads"),
         db_name=db_name,
     )
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index 540e114b82..c88cc18788 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -142,6 +142,10 @@ class PresenceHandler(BaseHandler):
 
     @defer.inlineCallbacks
     def is_presence_visible(self, observer_user, observed_user):
+        defer.returnValue(True)
+        return
+        # FIXME (erikj): This code path absolutely kills the database.
+
         assert(observed_user.is_mine)
 
         if observer_user == observed_user:
@@ -187,6 +191,10 @@ class PresenceHandler(BaseHandler):
 
     @defer.inlineCallbacks
     def set_state(self, target_user, auth_user, state):
+        return
+        # TODO (erikj): Turn this back on. Why did we end up sending EDUs
+        # everywhere?
+
         if not target_user.is_mine:
             raise SynapseError(400, "User is not hosted on this Home Server")
 
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 899b653fb7..8ab0b8033e 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -307,7 +307,7 @@ class MessageHandler(BaseHandler):
 
         ret = {"rooms": rooms_ret, "presence": presence[0], "end": now_token}
 
-        logger.debug("snapshot_all_rooms returning: %s", ret)
+        # logger.debug("snapshot_all_rooms returning: %s", ret)
 
         defer.returnValue(ret)
 
diff --git a/synapse/http/server.py b/synapse/http/server.py
index d1f99460c1..66f966fcaa 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -325,7 +325,9 @@ class ContentRepoResource(resource.Resource):
 
             # FIXME (erikj): These should use constants.
             file_name = os.path.basename(fname)
-            url = "http://%s/matrix/content/%s" % (self.hs.hostname, file_name)
+            url = "http://%s/matrix/content/%s" % (
+                self.hs.domain_with_port, file_name
+            )
 
             respond_with_json_bytes(request, 200,
                                     json.dumps({"content_token": url}),
diff --git a/synapse/rest/register.py b/synapse/rest/register.py
index eb457562b9..f17ec11cf4 100644
--- a/synapse/rest/register.py
+++ b/synapse/rest/register.py
@@ -33,10 +33,10 @@ class RegisterRestServlet(RestServlet):
         try:
             register_json = json.loads(request.content.read())
             if "password" in register_json:
-                password = register_json["password"]
+                password = register_json["password"].encode("utf-8")
 
             if type(register_json["user_id"]) == unicode:
-                desired_user_id = register_json["user_id"]
+                desired_user_id = register_json["user_id"].encode("utf-8")
                 if urllib.quote(desired_user_id) != desired_user_id:
                     raise SynapseError(
                         400,
diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py
index 89c87290cf..aca5cff737 100644
--- a/synapse/storage/roommember.py
+++ b/synapse/storage/roommember.py
@@ -145,7 +145,7 @@ class RoomMemberStore(SQLBaseStore):
 
         rows = yield self._execute_and_decode(sql, *where_values)
 
-        logger.debug("_get_members_query Got rows %s", rows)
+        # logger.debug("_get_members_query Got rows %s", rows)
 
         results = [self._parse_event_from_row(r) for r in rows]
         defer.returnValue(results)
diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql
index 39a1ed703e..e92f21ef3b 100644
--- a/synapse/storage/schema/im.sql
+++ b/synapse/storage/schema/im.sql
@@ -26,6 +26,11 @@ CREATE TABLE IF NOT EXISTS events(
     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,
@@ -34,6 +39,12 @@ CREATE TABLE IF NOT EXISTS state_events(
     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,
@@ -42,6 +53,11 @@ CREATE TABLE IF NOT EXISTS current_state_events(
     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,
@@ -50,6 +66,10 @@ CREATE TABLE IF NOT EXISTS room_memberships(
     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,
@@ -78,5 +98,6 @@ CREATE TABLE IF NOT EXISTS rooms(
 
 CREATE TABLE IF NOT EXISTS room_hosts(
     room_id TEXT NOT NULL,
-    host TEXT NOT NULL
+    host TEXT NOT NULL,
+    CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
 );
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index 8b88c49a0b..6d3cd76dba 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -190,6 +190,7 @@ class PresenceStateTestCase(unittest.TestCase):
             ),
             SynapseError
         )
+    test_get_disallowed_state.skip = "Presence polling is disabled"
 
     @defer.inlineCallbacks
     def test_set_my_state(self):
@@ -214,6 +215,7 @@ class PresenceStateTestCase(unittest.TestCase):
                 state={"state": OFFLINE})
 
         self.mock_stop.assert_called_with(self.u_apple)
+    test_set_my_state.skip = "Presence polling is disabled"
 
 
 class PresenceInvitesTestCase(unittest.TestCase):
@@ -653,6 +655,7 @@ class PresencePushTestCase(unittest.TestCase):
                     observed_user=self.u_banana,
                     statuscache=ANY), # self-reflection
         ]) # and no others...
+    test_push_local.skip = "Presence polling is disabled"
 
     @defer.inlineCallbacks
     def test_push_remote(self):
@@ -704,6 +707,7 @@ class PresencePushTestCase(unittest.TestCase):
         )
 
         yield put_json.await_calls()
+    test_push_remote.skip = "Presence polling is disabled"
 
     @defer.inlineCallbacks
     def test_recv_remote(self):
@@ -996,6 +1000,8 @@ class PresencePollingTestCase(unittest.TestCase):
 
         self.assertFalse("banana" in self.handler._local_pushmap)
         self.assertFalse("clementine" in self.handler._local_pushmap)
+    test_push_local.skip = "Presence polling is disabled"
+
 
     @defer.inlineCallbacks
     def test_remote_poll_send(self):
@@ -1044,6 +1050,7 @@ class PresencePollingTestCase(unittest.TestCase):
         put_json.await_calls()
 
         self.assertFalse(self.u_potato in self.handler._remote_recvmap)
+    test_remote_poll_send.skip = "Presence polling is disabled"
 
     @defer.inlineCallbacks
     def test_remote_poll_receive(self):
diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py
index bba5dd4e53..c25c6889be 100644
--- a/tests/handlers/test_presencelike.py
+++ b/tests/handlers/test_presencelike.py
@@ -135,6 +135,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
 
         mocked_set.assert_called_with("apple",
                 {"state": UNAVAILABLE, "status_msg": "Away"})
+    test_set_my_state.skip = "Presence polling is disabled"
 
     @defer.inlineCallbacks
     def test_push_local(self):
@@ -209,6 +210,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
             "displayname": "I am an Apple",
             "avatar_url": "http://foo",
         }, statuscache.state)
+    test_push_local.skip = "Presence polling is disabled"
+
 
     @defer.inlineCallbacks
     def test_push_remote(self):
@@ -239,6 +242,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
                     ],
                 },
         )
+    test_push_remote.skip = "Presence polling is disabled"
 
     @defer.inlineCallbacks
     def test_recv_remote(self):
diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py
index 8ac246b4d5..970405d271 100644
--- a/tests/rest/test_presence.py
+++ b/tests/rest/test_presence.py
@@ -114,6 +114,7 @@ class PresenceStateTestCase(unittest.TestCase):
         self.assertEquals(200, code)
         mocked_set.assert_called_with("apple",
                 {"state": UNAVAILABLE, "status_msg": "Away"})
+    test_set_my_status.skip = "Presence polling is disabled"
 
 
 class PresenceListTestCase(unittest.TestCase):
@@ -309,3 +310,4 @@ class PresenceEventStreamTestCase(unittest.TestCase):
                  "mtime_age": 0,
             }},
         ]}, response)
+    test_shortpoll.skip = "Presence polling is disabled"
diff --git a/webclient/app.css b/webclient/app.css
index 72b38cd950..dfa17fae62 100644
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -1,3 +1,71 @@
+/*** Mobile voodoo ***/
+@media all and (max-device-width: 640px) {
+            
+    #messageTableWrapper {
+        margin-right: 0px ! important;
+    }
+    
+    .leftBlock {
+        width: 8em ! important;
+    }
+    
+    #header,
+    #messageTable,
+    #wrapper,
+    #roomName,
+    #controls {
+        max-width: 640px ! important;
+    }    
+    
+    #userIdCell,
+    #usersTableWrapper,
+    #extraControls {
+        display: none;
+    }
+    
+    #buttonsCell {
+        width: 60px ! important;
+        padding-left: 20px ! important;
+    }
+    
+    #roomLogo {
+        display: none;
+    }
+    
+    #roomName {
+        text-align: left ! important;
+        top: -35px ! important;
+    }
+    
+    .bubble {
+        font-size: 12px ! important;
+        min-height: 20px ! important;
+    }
+    
+    #page {
+        top: 35px ! important;
+        bottom: 70px ! important;
+    }
+    
+    #header,
+    #page {
+        margin: 5px ! important;
+    }
+    
+    #header {
+        padding: 5px ! important;
+    }
+        
+    /* stop zoom on select */
+    select:focus,
+    textarea,
+    input
+    {
+        font-size: 16px ! important;
+    }
+    
+}
+
 body {
     font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
     font-size: 12pt;
@@ -17,7 +85,6 @@ h1 {
     left: 0px;
     right: 0px;
     margin: 20px;
-    margin: 20px;
 }
 
 #wrapper {
@@ -32,8 +99,7 @@ h1 {
     text-align: right;
     top: -40px;
     position: absolute;
-    font-size: 16pt;
-    margin-bottom: 10px;
+    font-size: 16px;
 }
 
 #controlPanel {
@@ -50,6 +116,10 @@ h1 {
     margin: auto;
 }
 
+#buttonsCell {
+    width: 150px;
+}
+
 #inputBarTable {
     width: 100%;
 }
@@ -111,13 +181,13 @@ h1 {
     color: #fff;
     margin: 2px;
     bottom: 0px;
-    font-size: 8pt;
+    font-size: 12px;
     word-break: break-all;
 }
 
 .userPresence {
     text-align: center;
-    font-size: 8pt;
+    font-size: 12px;
     color: #fff;
     background-color: #aaa;
     border-bottom: 1px #ddd solid;
@@ -159,7 +229,7 @@ h1 {
     background-color: #fff;
     color: #888;
     font-weight: medium;
-    font-size: 8pt;
+    font-size: 12px;
     text-align: right;
     border-top: 1px #ddd solid;
 }
@@ -277,7 +347,7 @@ h1 {
 .profile-avatar {
     width: 160px;
     height: 160px;
-    display:table-cell;
+    display: table-cell;
     vertical-align: middle;
     text-align: center;
 }
@@ -293,13 +363,19 @@ h1 {
 }
 
 #user-displayname {
-    font-size: 16pt;
+    font-size: 24px;
 }
 /******************************/
 
-#header {
-    padding-left: 20px;
-    padding-right: 20px;
+#header
+{
+    padding: 20px;
+    max-width: 1280px;
+    margin: auto;
+}
+
+#logo,
+#roomLogo {
     max-width: 1280px;
     margin: auto;
 }
diff --git a/webclient/index.html b/webclient/index.html
index ed1d9bb031..938d70c86d 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -2,10 +2,12 @@
 <html xmlns:ng="http://angularjs.org" ng-app="matrixWebClient" ng-controller="MatrixWebClientController">
 <head>
     <title>[matrix]</title>
-    
+        
     <link rel="stylesheet" href="app.css">
     <link rel="icon" href="favicon.ico">
    
+    <meta name="viewport" content="width=device-width">
+   
     <script type='text/javascript' src='js/jquery-1.8.3.min.js'></script> 
     <script src="js/angular.min.js"></script>
     <script src="js/angular-route.min.js"></script>
@@ -37,8 +39,6 @@
             <button ng-click='go("settings")'>Settings</button>
             <button ng-click="logout()">Log out</button>
         </div>
-
-        <h1>[matrix]</h1>
     </header>
 
     <div ng-view></div>
diff --git a/webclient/login/login.html b/webclient/login/login.html
index b1488b37f0..4b2ea60928 100644
--- a/webclient/login/login.html
+++ b/webclient/login/login.html
@@ -1,4 +1,6 @@
-<div ng-controller="LoginController" class="login">
+<div ng-controller="LoginController" class="login">    
+    <h1 id="logo">[matrix]</h1>
+
     <div id="page">
     <div id="wrapper">
 
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 95da067714..06ca63d2ea 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -1,4 +1,5 @@
 <div ng-controller="RoomController" data-ng-init="onInit()" class="room">
+    <h1 id="roomLogo">[matrix]</h1>
 
     <div id="page">
     <div id="wrapper">
@@ -32,7 +33,7 @@
                 ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
                 <td class="leftBlock">
                     <div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
-                    <div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm:ss' }}</div>
+                    <div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div>
                 </td>
                 <td class="avatar">
                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
@@ -73,29 +74,28 @@
         <div id="controls">
             <table id="inputBarTable">
                 <tr>
-                    <td width="1">
+                    <td id="userIdCell" width="1px">
                         {{ state.user_id }} 
                     </td>
-                    <td width="*" style="min-width: 100px">
+                    <td width="*">
                         <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
                     </td>
-                    <td width="150px">
+                    <td id="buttonsCell">
                         <button ng-click="send()">Send</button>
-                        <button m-file-input="imageFileToSend">Send Image</button>
-                    </td>
-                    <td width="1">
-                        
+                        <button m-file-input="imageFileToSend">Image</button>
                     </td>
                 </tr>
             </table>
 
-            <span>
-               Invite a user: 
-                    <input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>     
-                    <button ng-click="inviteUser(userIDToInvite)">Invite</button>
-            </span>
-            <button ng-click="leaveRoom()">Leave</button>
-            <button ng-click="loadMoreHistory()" ng-disabled="!state.can_paginate">Load more history</button>
+            <div id="extraControls">
+                <span>
+                   Invite a user: 
+                        <input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>     
+                        <button ng-click="inviteUser(userIDToInvite)">Invite</button>
+                </span>
+                <button ng-click="leaveRoom()">Leave</button>
+            </div>
+        
             {{ feedback }}
             <div ng-hide="!state.stream_failure">
                 {{ state.stream_failure.data.error || "Connection failure" }}
diff --git a/webclient/user/user.html b/webclient/user/user.html
index 47db09d1ee..4c91c8a48a 100644
--- a/webclient/user/user.html
+++ b/webclient/user/user.html
@@ -1,4 +1,5 @@
 <div ng-controller="UserController" class="user">
+    <h1 id="logo">[matrix]</h1>
 
     <div id="page">
     <div id="wrapper">