Merge remote-tracking branch 'origin/hotfixes-0.0.1' into develop

This commit is contained in:
Emmanuel ROHEE 2014-08-25 11:13:54 +02:00
commit 1bd380c816
24 changed files with 1787 additions and 40 deletions

View File

@ -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: Homeserver:
* Completely change the database schema to support generic event types. * Completely change the database schema to support generic event types.
* Improve presence reliability. * Improve presence reliability.
@ -18,3 +21,8 @@ Webclient:
* Use the new initial sync API to reduce number of round trips to the homeserver. * 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. * Change url scheme to use room aliases instead of room ids where known.
* Increase longpoll timeout. * Increase longpoll timeout.
Changes in synapse 0.0.0 (2014-08-13)
=====================================
* Initial alpha release

View File

@ -34,6 +34,10 @@ To get up and running:
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
say hi! :) say hi! :)
For more detailed setup instructions, please see further down this document.
[1] VoIP currently in development
About Matrix About Matrix
============ ============
@ -85,8 +89,6 @@ https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
Thanks for trying Matrix! Thanks for trying Matrix!
[1] VoIP currently in development
[2] Cryptographic signing of messages isn't turned on yet [2] Cryptographic signing of messages isn't turned on yet
[3] End-to-end encryption is currently in development [3] End-to-end encryption is currently in development

View File

@ -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"
}
}

View File

@ -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"
]
}
}
}
}
}

View File

@ -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"
}

View File

@ -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."
}
}
}
}
}
}

View File

@ -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."
}
}
}
}
}

View File

@ -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"
}

View File

@ -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"
}
}
}
}
}

View File

@ -37,6 +37,7 @@ import logging
import logging.config import logging.config
import sqlite3 import sqlite3
import os import os
import re
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -255,8 +256,14 @@ def setup():
logger.info("Server hostname: %s", args.host) 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( hs = SynapseHomeServer(
args.host, args.host,
domain_with_port=domain_with_port,
upload_dir=os.path.abspath("uploads"), upload_dir=os.path.abspath("uploads"),
db_name=db_name, db_name=db_name,
) )

View File

@ -142,6 +142,10 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def is_presence_visible(self, observer_user, observed_user): 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) assert(observed_user.is_mine)
if observer_user == observed_user: if observer_user == observed_user:
@ -187,6 +191,10 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def set_state(self, target_user, auth_user, state): 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: if not target_user.is_mine:
raise SynapseError(400, "User is not hosted on this Home Server") raise SynapseError(400, "User is not hosted on this Home Server")

View File

@ -307,7 +307,7 @@ class MessageHandler(BaseHandler):
ret = {"rooms": rooms_ret, "presence": presence[0], "end": now_token} 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) defer.returnValue(ret)

View File

@ -325,7 +325,9 @@ class ContentRepoResource(resource.Resource):
# FIXME (erikj): These should use constants. # FIXME (erikj): These should use constants.
file_name = os.path.basename(fname) 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, respond_with_json_bytes(request, 200,
json.dumps({"content_token": url}), json.dumps({"content_token": url}),

View File

@ -33,10 +33,10 @@ class RegisterRestServlet(RestServlet):
try: try:
register_json = json.loads(request.content.read()) register_json = json.loads(request.content.read())
if "password" in register_json: if "password" in register_json:
password = register_json["password"] password = register_json["password"].encode("utf-8")
if type(register_json["user_id"]) == unicode: 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: if urllib.quote(desired_user_id) != desired_user_id:
raise SynapseError( raise SynapseError(
400, 400,

View File

@ -145,7 +145,7 @@ class RoomMemberStore(SQLBaseStore):
rows = yield self._execute_and_decode(sql, *where_values) 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] results = [self._parse_event_from_row(r) for r in rows]
defer.returnValue(results) defer.returnValue(results)

View File

@ -26,6 +26,11 @@ CREATE TABLE IF NOT EXISTS events(
CONSTRAINT ev_uniq UNIQUE (event_id) 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( CREATE TABLE IF NOT EXISTS state_events(
event_id TEXT NOT NULL, event_id TEXT NOT NULL,
room_id TEXT NOT NULL, room_id TEXT NOT NULL,
@ -34,6 +39,12 @@ CREATE TABLE IF NOT EXISTS state_events(
prev_state TEXT 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( CREATE TABLE IF NOT EXISTS current_state_events(
event_id TEXT NOT NULL, event_id TEXT NOT NULL,
room_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 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( CREATE TABLE IF NOT EXISTS room_memberships(
event_id TEXT NOT NULL, event_id TEXT NOT NULL,
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
@ -50,6 +66,10 @@ CREATE TABLE IF NOT EXISTS room_memberships(
membership 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( CREATE TABLE IF NOT EXISTS feedback(
event_id TEXT NOT NULL, event_id TEXT NOT NULL,
feedback_type TEXT, feedback_type TEXT,
@ -78,5 +98,6 @@ CREATE TABLE IF NOT EXISTS rooms(
CREATE TABLE IF NOT EXISTS room_hosts( CREATE TABLE IF NOT EXISTS room_hosts(
room_id TEXT NOT NULL, room_id TEXT NOT NULL,
host TEXT NOT NULL host TEXT NOT NULL,
CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
); );

View File

@ -190,6 +190,7 @@ class PresenceStateTestCase(unittest.TestCase):
), ),
SynapseError SynapseError
) )
test_get_disallowed_state.skip = "Presence polling is disabled"
@defer.inlineCallbacks @defer.inlineCallbacks
def test_set_my_state(self): def test_set_my_state(self):
@ -214,6 +215,7 @@ class PresenceStateTestCase(unittest.TestCase):
state={"state": OFFLINE}) state={"state": OFFLINE})
self.mock_stop.assert_called_with(self.u_apple) self.mock_stop.assert_called_with(self.u_apple)
test_set_my_state.skip = "Presence polling is disabled"
class PresenceInvitesTestCase(unittest.TestCase): class PresenceInvitesTestCase(unittest.TestCase):
@ -653,6 +655,7 @@ class PresencePushTestCase(unittest.TestCase):
observed_user=self.u_banana, observed_user=self.u_banana,
statuscache=ANY), # self-reflection statuscache=ANY), # self-reflection
]) # and no others... ]) # and no others...
test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks @defer.inlineCallbacks
def test_push_remote(self): def test_push_remote(self):
@ -704,6 +707,7 @@ class PresencePushTestCase(unittest.TestCase):
) )
yield put_json.await_calls() yield put_json.await_calls()
test_push_remote.skip = "Presence polling is disabled"
@defer.inlineCallbacks @defer.inlineCallbacks
def test_recv_remote(self): def test_recv_remote(self):
@ -996,6 +1000,8 @@ class PresencePollingTestCase(unittest.TestCase):
self.assertFalse("banana" in self.handler._local_pushmap) self.assertFalse("banana" in self.handler._local_pushmap)
self.assertFalse("clementine" in self.handler._local_pushmap) self.assertFalse("clementine" in self.handler._local_pushmap)
test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks @defer.inlineCallbacks
def test_remote_poll_send(self): def test_remote_poll_send(self):
@ -1044,6 +1050,7 @@ class PresencePollingTestCase(unittest.TestCase):
put_json.await_calls() put_json.await_calls()
self.assertFalse(self.u_potato in self.handler._remote_recvmap) self.assertFalse(self.u_potato in self.handler._remote_recvmap)
test_remote_poll_send.skip = "Presence polling is disabled"
@defer.inlineCallbacks @defer.inlineCallbacks
def test_remote_poll_receive(self): def test_remote_poll_receive(self):

View File

@ -135,6 +135,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
mocked_set.assert_called_with("apple", mocked_set.assert_called_with("apple",
{"state": UNAVAILABLE, "status_msg": "Away"}) {"state": UNAVAILABLE, "status_msg": "Away"})
test_set_my_state.skip = "Presence polling is disabled"
@defer.inlineCallbacks @defer.inlineCallbacks
def test_push_local(self): def test_push_local(self):
@ -209,6 +210,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
"displayname": "I am an Apple", "displayname": "I am an Apple",
"avatar_url": "http://foo", "avatar_url": "http://foo",
}, statuscache.state) }, statuscache.state)
test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks @defer.inlineCallbacks
def test_push_remote(self): def test_push_remote(self):
@ -239,6 +242,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
], ],
}, },
) )
test_push_remote.skip = "Presence polling is disabled"
@defer.inlineCallbacks @defer.inlineCallbacks
def test_recv_remote(self): def test_recv_remote(self):

View File

@ -114,6 +114,7 @@ class PresenceStateTestCase(unittest.TestCase):
self.assertEquals(200, code) self.assertEquals(200, code)
mocked_set.assert_called_with("apple", mocked_set.assert_called_with("apple",
{"state": UNAVAILABLE, "status_msg": "Away"}) {"state": UNAVAILABLE, "status_msg": "Away"})
test_set_my_status.skip = "Presence polling is disabled"
class PresenceListTestCase(unittest.TestCase): class PresenceListTestCase(unittest.TestCase):
@ -309,3 +310,4 @@ class PresenceEventStreamTestCase(unittest.TestCase):
"mtime_age": 0, "mtime_age": 0,
}}, }},
]}, response) ]}, response)
test_shortpoll.skip = "Presence polling is disabled"

View File

@ -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 { body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif; font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt; font-size: 12pt;
@ -17,7 +85,6 @@ h1 {
left: 0px; left: 0px;
right: 0px; right: 0px;
margin: 20px; margin: 20px;
margin: 20px;
} }
#wrapper { #wrapper {
@ -32,8 +99,7 @@ h1 {
text-align: right; text-align: right;
top: -40px; top: -40px;
position: absolute; position: absolute;
font-size: 16pt; font-size: 16px;
margin-bottom: 10px;
} }
#controlPanel { #controlPanel {
@ -50,6 +116,10 @@ h1 {
margin: auto; margin: auto;
} }
#buttonsCell {
width: 150px;
}
#inputBarTable { #inputBarTable {
width: 100%; width: 100%;
} }
@ -111,13 +181,13 @@ h1 {
color: #fff; color: #fff;
margin: 2px; margin: 2px;
bottom: 0px; bottom: 0px;
font-size: 8pt; font-size: 12px;
word-break: break-all; word-break: break-all;
} }
.userPresence { .userPresence {
text-align: center; text-align: center;
font-size: 8pt; font-size: 12px;
color: #fff; color: #fff;
background-color: #aaa; background-color: #aaa;
border-bottom: 1px #ddd solid; border-bottom: 1px #ddd solid;
@ -159,7 +229,7 @@ h1 {
background-color: #fff; background-color: #fff;
color: #888; color: #888;
font-weight: medium; font-weight: medium;
font-size: 8pt; font-size: 12px;
text-align: right; text-align: right;
border-top: 1px #ddd solid; border-top: 1px #ddd solid;
} }
@ -277,7 +347,7 @@ h1 {
.profile-avatar { .profile-avatar {
width: 160px; width: 160px;
height: 160px; height: 160px;
display:table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
} }
@ -293,13 +363,19 @@ h1 {
} }
#user-displayname { #user-displayname {
font-size: 16pt; font-size: 24px;
} }
/******************************/ /******************************/
#header { #header
padding-left: 20px; {
padding-right: 20px; padding: 20px;
max-width: 1280px;
margin: auto;
}
#logo,
#roomLogo {
max-width: 1280px; max-width: 1280px;
margin: auto; margin: auto;
} }

View File

@ -6,6 +6,8 @@
<link rel="stylesheet" href="app.css"> <link rel="stylesheet" href="app.css">
<link rel="icon" href="favicon.ico"> <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 type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
<script src="js/angular.min.js"></script> <script src="js/angular.min.js"></script>
<script src="js/angular-route.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='go("settings")'>Settings</button>
<button ng-click="logout()">Log out</button> <button ng-click="logout()">Log out</button>
</div> </div>
<h1>[matrix]</h1>
</header> </header>
<div ng-view></div> <div ng-view></div>

View File

@ -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="page">
<div id="wrapper"> <div id="wrapper">

View File

@ -1,4 +1,5 @@
<div ng-controller="RoomController" data-ng-init="onInit()" class="room"> <div ng-controller="RoomController" data-ng-init="onInit()" class="room">
<h1 id="roomLogo">[matrix]</h1>
<div id="page"> <div id="page">
<div id="wrapper"> <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> 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"> <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="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>
<td class="avatar"> <td class="avatar">
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32" <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"> <div id="controls">
<table id="inputBarTable"> <table id="inputBarTable">
<tr> <tr>
<td width="1"> <td id="userIdCell" width="1px">
{{ state.user_id }} {{ state.user_id }}
</td> </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/> <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
</td> </td>
<td width="150px"> <td id="buttonsCell">
<button ng-click="send()">Send</button> <button ng-click="send()">Send</button>
<button m-file-input="imageFileToSend">Send Image</button> <button m-file-input="imageFileToSend">Image</button>
</td>
<td width="1">
</td> </td>
</tr> </tr>
</table> </table>
<span> <div id="extraControls">
Invite a user: <span>
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/> Invite a user:
<button ng-click="inviteUser(userIDToInvite)">Invite</button> <input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
</span> <button ng-click="inviteUser(userIDToInvite)">Invite</button>
<button ng-click="leaveRoom()">Leave</button> </span>
<button ng-click="loadMoreHistory()" ng-disabled="!state.can_paginate">Load more history</button> <button ng-click="leaveRoom()">Leave</button>
</div>
{{ feedback }} {{ feedback }}
<div ng-hide="!state.stream_failure"> <div ng-hide="!state.stream_failure">
{{ state.stream_failure.data.error || "Connection failure" }} {{ state.stream_failure.data.error || "Connection failure" }}

View File

@ -1,4 +1,5 @@
<div ng-controller="UserController" class="user"> <div ng-controller="UserController" class="user">
<h1 id="logo">[matrix]</h1>
<div id="page"> <div id="page">
<div id="wrapper"> <div id="wrapper">