mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-10-01 11:49:51 -04:00
Merge branch 'develop' into storage_transactions
This commit is contained in:
commit
1d95e78759
2
.gitignore
vendored
2
.gitignore
vendored
@ -10,7 +10,7 @@ docs/build/
|
||||
*.egg-info
|
||||
|
||||
cmdclient_config.json
|
||||
homeserver.db
|
||||
homeserver*.db
|
||||
|
||||
.coverage
|
||||
htmlcov
|
||||
|
@ -471,7 +471,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
room_name = args["vis"]
|
||||
body["room_alias_name"] = room_name
|
||||
|
||||
reactor.callFromThread(self._run_and_pprint, "POST", "/rooms", body)
|
||||
reactor.callFromThread(self._run_and_pprint, "POST", "/createRoom", body)
|
||||
|
||||
def do_raw(self, line):
|
||||
"""Directly send a JSON object: "raw <method> <path> <data> <notoken>"
|
||||
|
@ -21,6 +21,14 @@
|
||||
{
|
||||
"path": "/presence",
|
||||
"description": "Presence operations"
|
||||
},
|
||||
{
|
||||
"path": "/events",
|
||||
"description": "Event operations"
|
||||
},
|
||||
{
|
||||
"path": "/directory",
|
||||
"description": "Directory operations"
|
||||
}
|
||||
],
|
||||
"authorizations": {
|
||||
|
83
docs/client-server/swagger_matrix/directory
Normal file
83
docs/client-server/swagger_matrix/directory
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/directory",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/directory/room/{roomAlias}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get the room ID corresponding to this room alias.",
|
||||
"type": "DirectoryResponse",
|
||||
"nickname": "get_room_id_for_alias",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Create a new mapping from room alias to room ID.",
|
||||
"type": "void",
|
||||
"nickname": "add_room_alias",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The room ID to set.",
|
||||
"required": true,
|
||||
"type": "RoomAliasRequest",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"DirectoryResponse": {
|
||||
"id": "DirectoryResponse",
|
||||
"properties": {
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The fully-qualified room ID.",
|
||||
"required": true
|
||||
},
|
||||
"servers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "string"
|
||||
},
|
||||
"description": "A list of servers that know about this room.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"RoomAliasRequest": {
|
||||
"id": "RoomAliasRequest",
|
||||
"properties": {
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room ID to map the alias to.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,299 +1,246 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://petstore.swagger.wordnik.com/api",
|
||||
"resourcePath": "/user",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/events",
|
||||
"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",
|
||||
"path": "/events",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Logs out current logged in user session",
|
||||
"notes": "",
|
||||
"type": "void",
|
||||
"nickname": "logoutUser",
|
||||
"authorizations": {},
|
||||
"parameters": []
|
||||
"summary": "Listen on the event stream",
|
||||
"notes": "This can only be done by the logged in user. This will block until an event is received, or until the timeout is reached.",
|
||||
"type": "PaginationChunk",
|
||||
"nickname": "get_event_stream"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/createWithArray",
|
||||
"operations": [
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"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": "",
|
||||
"name": "from",
|
||||
"description": "The token to stream from.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nickname": "loginUser",
|
||||
"authorizations": {},
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"description": "The maximum time in milliseconds to wait for an event.",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"paramType": "query"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad pagination token."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/events/{eventId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get information about a single event.",
|
||||
"notes": "Get information about a single event.",
|
||||
"type": "Event",
|
||||
"nickname": "get_event",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "The user name for login",
|
||||
"name": "eventId",
|
||||
"description": "The event ID to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "The password for login in clear text",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username and password combination"
|
||||
"code": 404,
|
||||
"message": "Event not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/initialSync",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get this user's current state.",
|
||||
"notes": "Get this user's current state.",
|
||||
"type": "InitialSyncResponse",
|
||||
"nickname": "initial_sync",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "The maximum number of messages to return for each room.",
|
||||
"type": "integer",
|
||||
"paramType": "query",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/publicRooms",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of publicly visible rooms.",
|
||||
"type": "PublicRoomsPaginationChunk",
|
||||
"nickname": "get_public_room_list"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"User": {
|
||||
"id": "User",
|
||||
"PaginationChunk": {
|
||||
"id": "PaginationChunk",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "An array of events.",
|
||||
"required": true,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Event": {
|
||||
"id": "Event",
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "An ID which uniquely identifies this event.",
|
||||
"required": true
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room in which this event occurred.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"PublicRoomInfo": {
|
||||
"id": "PublicRoomInfo",
|
||||
"properties": {
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"description": "A list of room aliases for this room.",
|
||||
"items": {
|
||||
"$ref": "string"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the room, as given by the m.room.name state event."
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room ID for this public room.",
|
||||
"required": true
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"description": "The topic of this room, as given by the m.room.topic state event."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PublicRoomsPaginationChunk": {
|
||||
"id": "PublicRoomsPaginationChunk",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"userStatus": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "User Status",
|
||||
"enum": [
|
||||
"1-registered",
|
||||
"2-active",
|
||||
"3-closed"
|
||||
]
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "A list of public room data.",
|
||||
"required": true,
|
||||
"items": {
|
||||
"$ref": "PublicRoomInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitialSyncResponse": {
|
||||
"id": "InitialSyncResponse",
|
||||
"properties": {
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A streaming token which can be used with /events to continue from this snapshot of data.",
|
||||
"required": true
|
||||
},
|
||||
"presence": {
|
||||
"type": "array",
|
||||
"description": "A list of presence events.",
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
"rooms": {
|
||||
"type": "array",
|
||||
"description": "A list of initial sync room data.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "InitialSyncRoomData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitialSyncRoomData": {
|
||||
"id": "InitialSyncRoomData",
|
||||
"properties": {
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"description": "This user's membership state in this room.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The ID of this room.",
|
||||
"required": true
|
||||
},
|
||||
"messages": {
|
||||
"type": "PaginationChunk",
|
||||
"description": "The most recent messages for this room, governed by the limit parameter.",
|
||||
"required": false
|
||||
},
|
||||
"state": {
|
||||
"type": "array",
|
||||
"description": "A list of state events representing the current state of the room.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/presence_list/{userId}",
|
||||
"path": "/presence/list/{userId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
|
@ -14,13 +14,103 @@
|
||||
},
|
||||
"apis": [
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/{userId}/{messageId}",
|
||||
"path": "/rooms/{roomId}/send/{eventType}/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a generic non-state event to this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/{eventType}",
|
||||
"type": "EventId",
|
||||
"nickname": "send_non_state_event",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The event contents",
|
||||
"required": true,
|
||||
"type": "EventContent",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "eventType",
|
||||
"description": "The type of event to send.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state/{eventType}/{stateKey}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a generic state event to this room.",
|
||||
"notes": "The state key can be omitted, such that you can PUT to /rooms/{roomId}/state/{eventType}. The state key defaults to a 0 length string in this case.",
|
||||
"type": "void",
|
||||
"nickname": "send_state_event",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The event contents",
|
||||
"required": true,
|
||||
"type": "EventContent",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "eventType",
|
||||
"description": "The type of event to send.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "stateKey",
|
||||
"description": "An identifier used to specify clobbering semantics. State events with the same (roomId, eventType, stateKey) will be replaced.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/send/m.room.message/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a message in this room.",
|
||||
"notes": "Send a message in this room.",
|
||||
"type": "void",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message",
|
||||
"type": "EventId",
|
||||
"nickname": "send_message",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
@ -41,67 +131,18 @@
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"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",
|
||||
"path": "/rooms/{roomId}/state/m.room.topic",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
@ -127,12 +168,6 @@
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Must send messages as yourself."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -160,13 +195,13 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/{msgSenderId}/{messageId}/feedback/{senderId}/{feedbackType}",
|
||||
"path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send feedback to a message.",
|
||||
"notes": "Send feedback to a message.",
|
||||
"type": "void",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback",
|
||||
"type": "EventId",
|
||||
"nickname": "send_feedback",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
@ -187,107 +222,124 @@
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "msgSenderId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"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",
|
||||
"path": "/rooms/{roomId}/invite/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Invite a user to this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/invite",
|
||||
"type": "void",
|
||||
"nickname": "invite",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The user to invite.",
|
||||
"required": true,
|
||||
"type": "InviteRequest",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/join/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Join this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/join",
|
||||
"type": "void",
|
||||
"nickname": "join_room",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to join.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/leave/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Leave this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/leave",
|
||||
"type": "void",
|
||||
"nickname": "leave",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to leave.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state/m.room.member/{userId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
@ -376,58 +428,25 @@
|
||||
"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}",
|
||||
"path": "/join/{roomAliasOrId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Join a room via a room alias.",
|
||||
"notes": "Join a room via a room alias.",
|
||||
"summary": "Join a room via a room alias or room ID.",
|
||||
"notes": "Join a room via a room alias or room ID.",
|
||||
"type": "RoomInfo",
|
||||
"nickname": "join_room_via_alias",
|
||||
"nickname": "join",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias to join.",
|
||||
"name": "roomAliasOrId",
|
||||
"description": "The room alias or room ID to join.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
@ -443,7 +462,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms",
|
||||
"path": "/createRoom",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
@ -477,7 +496,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/list",
|
||||
"path": "/rooms/{roomId}/messages",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
@ -519,7 +538,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/members/list",
|
||||
"path": "/rooms/{roomId}/members",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
@ -704,12 +723,17 @@
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "An ID which uniquely identifies this event.",
|
||||
"description": "An ID which uniquely identifies this event. This is automatically set by the server.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room in which this event occurred.",
|
||||
"description": "The room in which this event occurred. This is automatically set by the server.",
|
||||
"required": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The event type.",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
@ -717,6 +741,26 @@
|
||||
"MessageEvent"
|
||||
]
|
||||
},
|
||||
"EventId": {
|
||||
"id": "EventId",
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "The allocated event ID for this event.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"EventContent": {
|
||||
"id": "EventContent",
|
||||
"properties": {
|
||||
"__event_content_keys__": {
|
||||
"type": "string",
|
||||
"description": "Event-specific content keys and values.",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessageEvent": {
|
||||
"id": "MessageEvent",
|
||||
"properties": {
|
||||
@ -733,73 +777,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tag": {
|
||||
"id": "Tag",
|
||||
"InviteRequest": {
|
||||
"id": "InviteRequest",
|
||||
"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": {
|
||||
"user_id": {
|
||||
"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"
|
||||
"description": "The fully-qualified user ID."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,92 +0,0 @@
|
||||
=========================
|
||||
Client-Server URL Summary
|
||||
=========================
|
||||
|
||||
A brief overview of the URL scheme involved in the Synapse Client-Server API.
|
||||
|
||||
|
||||
URLs
|
||||
====
|
||||
|
||||
Fetch events:
|
||||
GET /events
|
||||
|
||||
Registering an account
|
||||
POST /register
|
||||
|
||||
Unregistering an account
|
||||
POST /unregister
|
||||
|
||||
Rooms
|
||||
-----
|
||||
|
||||
Creating a room by ID
|
||||
PUT /rooms/$roomid
|
||||
|
||||
Creating an anonymous room
|
||||
POST /rooms
|
||||
|
||||
Room topic
|
||||
GET /rooms/$roomid/topic
|
||||
PUT /rooms/$roomid/topic
|
||||
|
||||
List rooms
|
||||
GET /rooms/list
|
||||
|
||||
Invite/Join/Leave
|
||||
GET /rooms/$roomid/members/$userid/state
|
||||
PUT /rooms/$roomid/members/$userid/state
|
||||
DELETE /rooms/$roomid/members/$userid/state
|
||||
|
||||
List members
|
||||
GET /rooms/$roomid/members/list
|
||||
|
||||
Sending/reading messages
|
||||
PUT /rooms/$roomid/messages/$sender/$msgid
|
||||
|
||||
Feedback
|
||||
GET /rooms/$roomid/messages/$sender/$msgid/feedback/$feedbackuser/$feedback
|
||||
PUT /rooms/$roomid/messages/$sender/$msgid/feedback/$feedbackuser/$feedback
|
||||
|
||||
Paginating messages
|
||||
GET /rooms/$roomid/messages/list
|
||||
|
||||
Profiles
|
||||
--------
|
||||
|
||||
Display name
|
||||
GET /profile/$userid/displayname
|
||||
PUT /profile/$userid/displayname
|
||||
|
||||
Avatar URL
|
||||
GET /profile/$userid/avatar_url
|
||||
PUT /profile/$userid/avatar_url
|
||||
|
||||
Metadata
|
||||
GET /profile/$userid/metadata
|
||||
POST /profile/$userid/metadata
|
||||
|
||||
Presence
|
||||
--------
|
||||
|
||||
My state or status message
|
||||
GET /presence/$userid/status
|
||||
PUT /presence/$userid/status
|
||||
also 'GET' for fetching others
|
||||
|
||||
TODO(paul): per-device idle time, device type; similar to above
|
||||
|
||||
My presence list
|
||||
GET /presence_list/$myuserid
|
||||
POST /presence_list/$myuserid
|
||||
body is JSON-encoded dict of keys:
|
||||
invite: list of UserID strings to invite
|
||||
drop: list of UserID strings to remove
|
||||
TODO(paul): define other ops: accept, group management, ordering?
|
||||
|
||||
Presence polling start/stop
|
||||
POST /presence_list/$myuserid?op=start
|
||||
POST /presence_list/$myuserid?op=stop
|
||||
|
||||
Presence invite
|
||||
POST /presence_list/$myuserid/invite/$targetuserid
|
@ -43,6 +43,22 @@ import re
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SCHEMAS = [
|
||||
"transactions",
|
||||
"pdu",
|
||||
"users",
|
||||
"profiles",
|
||||
"presence",
|
||||
"im",
|
||||
"room_aliases",
|
||||
]
|
||||
|
||||
|
||||
# Remember to update this number every time an incompatible change is made to
|
||||
# database schema files, so the users will be informed on server restarts.
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
class SynapseHomeServer(HomeServer):
|
||||
|
||||
def build_http_client(self):
|
||||
@ -65,31 +81,39 @@ class SynapseHomeServer(HomeServer):
|
||||
don't have to worry about overwriting existing content.
|
||||
"""
|
||||
logging.info("Preparing database: %s...", self.db_name)
|
||||
|
||||
with sqlite3.connect(self.db_name) as db_conn:
|
||||
c = db_conn.cursor()
|
||||
c.execute("PRAGMA user_version")
|
||||
row = c.fetchone()
|
||||
|
||||
if row and row[0]:
|
||||
user_version = row[0]
|
||||
|
||||
if user_version < SCHEMA_VERSION:
|
||||
# TODO(paul): add some kind of intelligent fixup here
|
||||
raise ValueError("Cannot use this database as the " +
|
||||
"schema version (%d) does not match (%d)" %
|
||||
(user_version, SCHEMA_VERSION)
|
||||
)
|
||||
|
||||
else:
|
||||
for sql_loc in SCHEMAS:
|
||||
sql_script = read_schema(sql_loc)
|
||||
|
||||
c.executescript(sql_script)
|
||||
db_conn.commit()
|
||||
|
||||
c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
|
||||
|
||||
c.close()
|
||||
|
||||
logging.info("Database prepared in %s.", self.db_name)
|
||||
|
||||
pool = adbapi.ConnectionPool(
|
||||
'sqlite3', self.db_name, check_same_thread=False,
|
||||
cp_min=1, cp_max=1)
|
||||
|
||||
schemas = [
|
||||
"transactions",
|
||||
"pdu",
|
||||
"users",
|
||||
"profiles",
|
||||
"presence",
|
||||
"im",
|
||||
"room_aliases",
|
||||
]
|
||||
|
||||
for sql_loc in schemas:
|
||||
sql_script = read_schema(sql_loc)
|
||||
|
||||
with sqlite3.connect(self.db_name) as db_conn:
|
||||
c = db_conn.cursor()
|
||||
c.executescript(sql_script)
|
||||
c.close()
|
||||
db_conn.commit()
|
||||
|
||||
logging.info("Database prepared in %s.", self.db_name)
|
||||
|
||||
return pool
|
||||
|
||||
def create_resource_tree(self, web_client, redirect_root_to_web_client):
|
||||
@ -184,6 +208,7 @@ class SynapseHomeServer(HomeServer):
|
||||
|
||||
def start_listening(self, port):
|
||||
reactor.listenTCP(port, Site(self.root_resource))
|
||||
logger.info("Synapse now listening on port %d", port)
|
||||
|
||||
|
||||
def setup_logging(verbosity=0, filename=None, config_path=None):
|
||||
@ -278,7 +303,7 @@ def setup():
|
||||
redirect_root_to_web_client=True)
|
||||
hs.start_listening(args.port)
|
||||
|
||||
hs.build_db_pool()
|
||||
hs.get_db_pool()
|
||||
|
||||
if args.manhole:
|
||||
f = twisted.manhole.telnet.ShellFactory()
|
||||
|
@ -23,6 +23,7 @@ from .login import LoginHandler
|
||||
from .profile import ProfileHandler
|
||||
from .presence import PresenceHandler
|
||||
from .directory import DirectoryHandler
|
||||
from .typing import TypingNotificationHandler
|
||||
|
||||
|
||||
class Handlers(object):
|
||||
@ -46,3 +47,4 @@ class Handlers(object):
|
||||
self.room_list_handler = RoomListHandler(hs)
|
||||
self.login_handler = LoginHandler(hs)
|
||||
self.directory_handler = DirectoryHandler(hs)
|
||||
self.typing_notification_handler = TypingNotificationHandler(hs)
|
||||
|
146
synapse/handlers/typing.py
Normal file
146
synapse/handlers/typing.py
Normal file
@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from ._base import BaseHandler
|
||||
|
||||
import logging
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# A tiny object useful for storing a user's membership in a room, as a mapping
|
||||
# key
|
||||
RoomMember = namedtuple("RoomMember", ("room_id", "user"))
|
||||
|
||||
|
||||
class TypingNotificationHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(TypingNotificationHandler, self).__init__(hs)
|
||||
|
||||
self.homeserver = hs
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
self.federation = hs.get_replication_layer()
|
||||
|
||||
self.federation.register_edu_handler("m.typing", self._recv_edu)
|
||||
|
||||
self._member_typing_until = {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def started_typing(self, target_user, auth_user, room_id, timeout):
|
||||
if not target_user.is_mine:
|
||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||
|
||||
if target_user != auth_user:
|
||||
raise AuthError(400, "Cannot set another user's typing state")
|
||||
|
||||
until = self.clock.time_msec() + timeout
|
||||
member = RoomMember(room_id=room_id, user=target_user)
|
||||
|
||||
was_present = member in self._member_typing_until
|
||||
|
||||
self._member_typing_until[member] = until
|
||||
|
||||
if was_present:
|
||||
# No point sending another notification
|
||||
defer.returnValue(None)
|
||||
|
||||
yield self._push_update(
|
||||
room_id=room_id,
|
||||
user=target_user,
|
||||
typing=True,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def stopped_typing(self, target_user, auth_user, room_id):
|
||||
if not target_user.is_mine:
|
||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||
|
||||
if target_user != auth_user:
|
||||
raise AuthError(400, "Cannot set another user's typing state")
|
||||
|
||||
member = RoomMember(room_id=room_id, user=target_user)
|
||||
|
||||
if member not in self._member_typing_until:
|
||||
# No point
|
||||
defer.returnValue(None)
|
||||
|
||||
yield self._push_update(
|
||||
room_id=room_id,
|
||||
user=target_user,
|
||||
typing=False,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _push_update(self, room_id, user, typing):
|
||||
localusers = set()
|
||||
remotedomains = set()
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
yield rm_handler.fetch_room_distributions_into(room_id,
|
||||
localusers=localusers, remotedomains=remotedomains,
|
||||
ignore_user=user)
|
||||
|
||||
for u in localusers:
|
||||
self.push_update_to_clients(
|
||||
room_id=room_id,
|
||||
observer_user=u,
|
||||
observed_user=user,
|
||||
typing=typing,
|
||||
)
|
||||
|
||||
deferreds = []
|
||||
for domain in remotedomains:
|
||||
deferreds.append(self.federation.send_edu(
|
||||
destination=domain,
|
||||
edu_type="m.typing",
|
||||
content={
|
||||
"room_id": room_id,
|
||||
"user_id": user.to_string(),
|
||||
"typing": typing,
|
||||
},
|
||||
))
|
||||
|
||||
yield defer.DeferredList(deferreds, consumeErrors=False)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _recv_edu(self, origin, content):
|
||||
room_id = content["room_id"]
|
||||
user = self.homeserver.parse_userid(content["user_id"])
|
||||
|
||||
localusers = set()
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
yield rm_handler.fetch_room_distributions_into(room_id,
|
||||
localusers=localusers)
|
||||
|
||||
for u in localusers:
|
||||
self.push_update_to_clients(
|
||||
room_id=room_id,
|
||||
observer_user=u,
|
||||
observed_user=user,
|
||||
typing=content["typing"]
|
||||
)
|
||||
|
||||
def push_update_to_clients(self, room_id, observer_user, observed_user,
|
||||
typing):
|
||||
# TODO(paul) steal this from presence.py
|
||||
pass
|
@ -15,8 +15,7 @@
|
||||
|
||||
|
||||
from . import (
|
||||
room, events, register, login, profile, public, presence, initial_sync,
|
||||
directory
|
||||
room, events, register, login, profile, presence, initial_sync, directory
|
||||
)
|
||||
|
||||
|
||||
@ -40,7 +39,6 @@ class RestServletFactory(object):
|
||||
register.register_servlets(hs, client_resource)
|
||||
login.register_servlets(hs, client_resource)
|
||||
profile.register_servlets(hs, client_resource)
|
||||
public.register_servlets(hs, client_resource)
|
||||
presence.register_servlets(hs, client_resource)
|
||||
initial_sync.register_servlets(hs, client_resource)
|
||||
directory.register_servlets(hs, client_resource)
|
||||
|
@ -31,7 +31,7 @@ def register_servlets(hs, http_server):
|
||||
|
||||
|
||||
class ClientDirectoryServer(RestServlet):
|
||||
PATTERN = client_path_pattern("/ds/room/(?P<room_alias>[^/]*)$")
|
||||
PATTERN = client_path_pattern("/directory/room/(?P<room_alias>[^/]*)$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_alias):
|
||||
|
@ -68,7 +68,7 @@ class PresenceStatusRestServlet(RestServlet):
|
||||
|
||||
|
||||
class PresenceListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/presence_list/(?P<user_id>[^/]*)")
|
||||
PATTERN = client_path_pattern("/presence/list/(?P<user_id>[^/]*)")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, user_id):
|
||||
|
@ -1,33 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module contains REST servlets to do with public paths: /public"""
|
||||
from twisted.internet import defer
|
||||
|
||||
from base import RestServlet, client_path_pattern
|
||||
|
||||
|
||||
class PublicRoomListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/public/rooms$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
handler = self.handlers.room_list_handler
|
||||
data = yield handler.get_public_room_list()
|
||||
defer.returnValue((200, data))
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
PublicRoomListRestServlet(hs).register(http_server)
|
@ -34,31 +34,28 @@ class RoomCreateRestServlet(RestServlet):
|
||||
# No PATTERN; we have custom dispatch rules here
|
||||
|
||||
def register(self, http_server):
|
||||
# /rooms OR /rooms/<roomid>
|
||||
http_server.register_path("POST",
|
||||
client_path_pattern("/rooms$"),
|
||||
self.on_POST)
|
||||
http_server.register_path("PUT",
|
||||
client_path_pattern(
|
||||
"/rooms/(?P<room_id>[^/]*)$"),
|
||||
self.on_PUT)
|
||||
PATTERN = "/createRoom"
|
||||
register_txn_path(self, PATTERN, http_server)
|
||||
# define CORS for all of /rooms in RoomCreateRestServlet for simplicity
|
||||
http_server.register_path("OPTIONS",
|
||||
client_path_pattern("/rooms(?:/.*)?$"),
|
||||
self.on_OPTIONS)
|
||||
# define CORS for /createRoom[/txnid]
|
||||
http_server.register_path("OPTIONS",
|
||||
client_path_pattern("/createRoom(?:/.*)?$"),
|
||||
self.on_OPTIONS)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id):
|
||||
room_id = urllib.unquote(room_id)
|
||||
auth_user = yield self.auth.get_user_by_req(request)
|
||||
def on_PUT(self, request, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not room_id:
|
||||
raise SynapseError(400, "PUT must specify a room ID")
|
||||
response = yield self.on_POST(request)
|
||||
|
||||
room_config = self.get_room_config(request)
|
||||
info = yield self.make_room(room_config, auth_user, room_id)
|
||||
room_config.update(info)
|
||||
defer.returnValue((200, info))
|
||||
self.txns.store_client_transaction(request, txn_id, response)
|
||||
defer.returnValue(response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
@ -267,6 +264,17 @@ class JoinRoomAliasServlet(RestServlet):
|
||||
defer.returnValue(response)
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class PublicRoomListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/publicRooms$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
handler = self.handlers.room_list_handler
|
||||
data = yield handler.get_public_room_list()
|
||||
defer.returnValue((200, data))
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class RoomMemberListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members$")
|
||||
@ -314,6 +322,50 @@ class RoomMessageListRestServlet(RestServlet):
|
||||
defer.returnValue((200, msgs))
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class RoomStateRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/state$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
# TODO: Get all the current state for this room and return in the same
|
||||
# format as initial sync, that is:
|
||||
# [
|
||||
# { state event }, { state event }
|
||||
# ]
|
||||
defer.returnValue((200, []))
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class RoomInitialSyncRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/initialSync$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
# TODO: Get all the initial sync data for this room and return in the
|
||||
# same format as initial sync, that is:
|
||||
# {
|
||||
# membership: join,
|
||||
# messages: [
|
||||
# chunk: [ msg events ],
|
||||
# start: s_tok,
|
||||
# end: e_tok
|
||||
# ],
|
||||
# room_id: foo,
|
||||
# state: [
|
||||
# { state event } , { state event }
|
||||
# ]
|
||||
# }
|
||||
# Probably worth keeping the keys room_id and membership for parity with
|
||||
# /initialSync even though they must be joined to sync this and know the
|
||||
# room ID, so clients can reuse the same code (room_id and membership
|
||||
# are MANDATORY for /initialSync, so the code will expect it to be
|
||||
# there)
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
|
||||
class RoomTriggerBackfill(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/backfill$")
|
||||
|
||||
@ -427,3 +479,6 @@ def register_servlets(hs, http_server):
|
||||
RoomTriggerBackfill(hs).register(http_server)
|
||||
RoomMembershipRestServlet(hs).register(http_server)
|
||||
RoomSendEventRestServlet(hs).register(http_server)
|
||||
PublicRoomListRestServlet(hs).register(http_server)
|
||||
RoomStateRestServlet(hs).register(http_server)
|
||||
RoomInitialSyncRestServlet(hs).register(http_server)
|
||||
|
@ -124,6 +124,11 @@ class BaseHomeServer(object):
|
||||
object."""
|
||||
return UserID.from_string(s, hs=self)
|
||||
|
||||
def parse_roomid(self, s):
|
||||
"""Parse the string given by 's' as a Room ID and return a RoomID
|
||||
object."""
|
||||
return RoomID.from_string(s, hs=self)
|
||||
|
||||
def parse_roomalias(self, s):
|
||||
"""Parse the string given by 's' as a Room Alias and return a RoomAlias
|
||||
object."""
|
||||
|
250
tests/handlers/test_typing.py
Normal file
250
tests/handlers/test_typing.py
Normal file
@ -0,0 +1,250 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import defer
|
||||
|
||||
from mock import Mock, call, ANY
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ..utils import MockHttpResource, MockClock, DeferredMockCallable
|
||||
|
||||
from synapse.server import HomeServer
|
||||
from synapse.handlers.typing import TypingNotificationHandler
|
||||
|
||||
|
||||
logging.getLogger().addHandler(logging.NullHandler())
|
||||
|
||||
|
||||
def _expect_edu(destination, edu_type, content, origin="test"):
|
||||
return {
|
||||
"origin": origin,
|
||||
"ts": 1000000,
|
||||
"pdus": [],
|
||||
"edus": [
|
||||
{
|
||||
"origin": origin,
|
||||
"destination": destination,
|
||||
"edu_type": edu_type,
|
||||
"content": content,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _make_edu_json(origin, edu_type, content):
|
||||
return json.dumps(_expect_edu("test", edu_type, content, origin=origin))
|
||||
|
||||
|
||||
class JustTypingNotificationHandlers(object):
|
||||
def __init__(self, hs):
|
||||
self.typing_notification_handler = TypingNotificationHandler(hs)
|
||||
|
||||
|
||||
class TypingNotificationsTestCase(unittest.TestCase):
|
||||
"""Tests typing notifications to rooms."""
|
||||
def setUp(self):
|
||||
self.clock = MockClock()
|
||||
|
||||
self.mock_http_client = Mock(spec=[])
|
||||
self.mock_http_client.put_json = DeferredMockCallable()
|
||||
|
||||
self.mock_federation_resource = MockHttpResource()
|
||||
|
||||
hs = HomeServer("test",
|
||||
clock=self.clock,
|
||||
db_pool=None,
|
||||
datastore=Mock(spec=[
|
||||
# Bits that Federation needs
|
||||
"prep_send_transaction",
|
||||
"delivered_txn",
|
||||
"get_received_txn_response",
|
||||
"set_received_txn_response",
|
||||
]),
|
||||
handlers=None,
|
||||
resource_for_client=Mock(),
|
||||
resource_for_federation=self.mock_federation_resource,
|
||||
http_client=self.mock_http_client,
|
||||
)
|
||||
hs.handlers = JustTypingNotificationHandlers(hs)
|
||||
|
||||
self.mock_update_client = Mock()
|
||||
self.mock_update_client.return_value = defer.succeed(None)
|
||||
|
||||
self.handler = hs.get_handlers().typing_notification_handler
|
||||
self.handler.push_update_to_clients = self.mock_update_client
|
||||
|
||||
self.datastore = hs.get_datastore()
|
||||
|
||||
def get_received_txn_response(*args):
|
||||
return defer.succeed(None)
|
||||
self.datastore.get_received_txn_response = get_received_txn_response
|
||||
|
||||
self.room_id = "a-room"
|
||||
|
||||
# Mock the RoomMemberHandler
|
||||
hs.handlers.room_member_handler = Mock(spec=[])
|
||||
self.room_member_handler = hs.handlers.room_member_handler
|
||||
|
||||
self.room_members = []
|
||||
|
||||
def get_rooms_for_user(user):
|
||||
if user in self.room_members:
|
||||
return defer.succeed([self.room_id])
|
||||
else:
|
||||
return defer.succeed([])
|
||||
self.room_member_handler.get_rooms_for_user = get_rooms_for_user
|
||||
|
||||
def get_room_members(room_id):
|
||||
if room_id == self.room_id:
|
||||
return defer.succeed(self.room_members)
|
||||
else:
|
||||
return defer.succeed([])
|
||||
self.room_member_handler.get_room_members = get_room_members
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def fetch_room_distributions_into(room_id, localusers=None,
|
||||
remotedomains=None, ignore_user=None):
|
||||
|
||||
members = yield get_room_members(room_id)
|
||||
for member in members:
|
||||
if ignore_user is not None and member == ignore_user:
|
||||
continue
|
||||
|
||||
if member.is_mine:
|
||||
if localusers is not None:
|
||||
localusers.add(member)
|
||||
else:
|
||||
if remotedomains is not None:
|
||||
remotedomains.add(member.domain)
|
||||
self.room_member_handler.fetch_room_distributions_into = (
|
||||
fetch_room_distributions_into)
|
||||
|
||||
# Some local users to test with
|
||||
self.u_apple = hs.parse_userid("@apple:test")
|
||||
self.u_banana = hs.parse_userid("@banana:test")
|
||||
|
||||
# Remote user
|
||||
self.u_onion = hs.parse_userid("@onion:farm")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_started_typing_local(self):
|
||||
self.room_members = [self.u_apple, self.u_banana]
|
||||
|
||||
yield self.handler.started_typing(
|
||||
target_user=self.u_apple,
|
||||
auth_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
timeout=20000,
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_banana,
|
||||
observed_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
typing=True),
|
||||
])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_started_typing_remote_send(self):
|
||||
self.room_members = [self.u_apple, self.u_onion]
|
||||
|
||||
put_json = self.mock_http_client.put_json
|
||||
put_json.expect_call_and_return(
|
||||
call("farm",
|
||||
path="/matrix/federation/v1/send/1000000/",
|
||||
data=_expect_edu("farm", "m.typing",
|
||||
content={
|
||||
"room_id": self.room_id,
|
||||
"user_id": self.u_apple.to_string(),
|
||||
"typing": True,
|
||||
}
|
||||
)
|
||||
),
|
||||
defer.succeed((200, "OK"))
|
||||
)
|
||||
|
||||
yield self.handler.started_typing(
|
||||
target_user=self.u_apple,
|
||||
auth_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
timeout=20000,
|
||||
)
|
||||
|
||||
yield put_json.await_calls()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_started_typing_remote_recv(self):
|
||||
self.room_members = [self.u_apple, self.u_onion]
|
||||
|
||||
yield self.mock_federation_resource.trigger("PUT",
|
||||
"/matrix/federation/v1/send/1000000/",
|
||||
_make_edu_json("farm", "m.typing",
|
||||
content={
|
||||
"room_id": self.room_id,
|
||||
"user_id": self.u_onion.to_string(),
|
||||
"typing": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_apple,
|
||||
observed_user=self.u_onion,
|
||||
room_id=self.room_id,
|
||||
typing=True),
|
||||
])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_stopped_typing(self):
|
||||
self.room_members = [self.u_apple, self.u_banana, self.u_onion]
|
||||
|
||||
put_json = self.mock_http_client.put_json
|
||||
put_json.expect_call_and_return(
|
||||
call("farm",
|
||||
path="/matrix/federation/v1/send/1000000/",
|
||||
data=_expect_edu("farm", "m.typing",
|
||||
content={
|
||||
"room_id": self.room_id,
|
||||
"user_id": self.u_apple.to_string(),
|
||||
"typing": False,
|
||||
}
|
||||
)
|
||||
),
|
||||
defer.succeed((200, "OK"))
|
||||
)
|
||||
|
||||
# Gut-wrenching
|
||||
from synapse.handlers.typing import RoomMember
|
||||
self.handler._member_typing_until[
|
||||
RoomMember(self.room_id, self.u_apple)
|
||||
] = 1002000
|
||||
|
||||
yield self.handler.stopped_typing(
|
||||
target_user=self.u_apple,
|
||||
auth_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_banana,
|
||||
observed_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
typing=False),
|
||||
])
|
||||
|
||||
yield put_json.await_calls()
|
@ -179,9 +179,8 @@ class EventStreamPermissionsTestCase(RestTestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_stream_room_permissions(self):
|
||||
room_id = "!rid1:test"
|
||||
yield self.create_room_as(room_id, self.other_user,
|
||||
tok=self.other_token)
|
||||
room_id = yield self.create_room_as(self.other_user,
|
||||
tok=self.other_token)
|
||||
yield self.send(room_id, tok=self.other_token)
|
||||
|
||||
# invited to room (expect no content for room)
|
||||
|
@ -171,7 +171,7 @@ class PresenceListTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("GET",
|
||||
"/presence_list/%s" % (myid), None)
|
||||
"/presence/list/%s" % (myid), None)
|
||||
|
||||
self.assertEquals(200, code)
|
||||
self.assertEquals(
|
||||
@ -192,7 +192,7 @@ class PresenceListTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("POST",
|
||||
"/presence_list/%s" % (myid),
|
||||
"/presence/list/%s" % (myid),
|
||||
"""{"invite": ["@banana:test"]}"""
|
||||
)
|
||||
|
||||
@ -212,7 +212,7 @@ class PresenceListTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("POST",
|
||||
"/presence_list/%s" % (myid),
|
||||
"/presence/list/%s" % (myid),
|
||||
"""{"drop": ["@banana:test"]}"""
|
||||
)
|
||||
|
||||
|
@ -74,13 +74,11 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
# create some rooms under the name rmcreator_id
|
||||
self.uncreated_rmid = "!aa:test"
|
||||
|
||||
self.created_rmid = "!abc:test"
|
||||
yield self.create_room_as(self.created_rmid, self.rmcreator_id,
|
||||
is_public=False)
|
||||
self.created_rmid = yield self.create_room_as(self.rmcreator_id,
|
||||
is_public=False)
|
||||
|
||||
self.created_public_rmid = "!def1234ghi:test"
|
||||
yield self.create_room_as(self.created_public_rmid, self.rmcreator_id,
|
||||
is_public=True)
|
||||
self.created_public_rmid = yield self.create_room_as(self.rmcreator_id,
|
||||
is_public=True)
|
||||
|
||||
# send a message in one of the rooms
|
||||
self.created_rmid_msg_path = ("/rooms/%s/send/m.room.message/a1" %
|
||||
@ -423,8 +421,7 @@ class RoomsMemberListTestCase(RestTestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_member_list(self):
|
||||
room_id = "!aa:test"
|
||||
yield self.create_room_as(room_id, self.user_id)
|
||||
room_id = yield self.create_room_as(self.user_id)
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/%s/members" % room_id)
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
@ -437,18 +434,16 @@ class RoomsMemberListTestCase(RestTestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_member_list_no_permission(self):
|
||||
room_id = "!bb:test"
|
||||
yield self.create_room_as(room_id, "@some_other_guy:red")
|
||||
room_id = yield self.create_room_as("@some_other_guy:red")
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/%s/members" % room_id)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_member_list_mixed_memberships(self):
|
||||
room_id = "!bb:test"
|
||||
room_creator = "@some_other_guy:blue"
|
||||
room_id = yield self.create_room_as(room_creator)
|
||||
room_path = "/rooms/%s/members" % room_id
|
||||
yield self.create_room_as(room_id, room_creator)
|
||||
yield self.invite(room=room_id, src=room_creator,
|
||||
targ=self.user_id)
|
||||
# can't see list if you're just invited.
|
||||
@ -503,107 +498,57 @@ class RoomsCreateTestCase(RestTestCase):
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_no_keys(self):
|
||||
# POST with no config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
"{}")
|
||||
(code, response) = yield self.mock_resource.trigger("POST",
|
||||
"/createRoom",
|
||||
"{}")
|
||||
self.assertEquals(200, code, response)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_visibility_key(self):
|
||||
# POST with visibility config key, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'{"visibility":"private"}')
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'{"visibility":"private"}')
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_custom_key(self):
|
||||
# POST with custom config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'{"custom":"stuff"}')
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'{"custom":"stuff"}')
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_known_and_unknown_keys(self):
|
||||
# POST with custom + known config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'{"visibility":"private","custom":"things"}')
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'{"visibility":"private","custom":"things"}')
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_invalid_content(self):
|
||||
# POST with invalid content / paths, expect 400
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'{"visibili')
|
||||
self.assertEquals(400, code)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'["hello"]')
|
||||
self.assertEquals(400, code)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_no_keys(self):
|
||||
# PUT with no config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21aa%3Atest", "{}"
|
||||
)
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_visibility_key(self):
|
||||
# PUT with known config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21bb%3Atest", '{"visibility":"private"}'
|
||||
)
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_custom_key(self):
|
||||
# PUT with custom config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21cc%3Atest", '{"custom":"stuff"}'
|
||||
)
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_known_and_unknown_keys(self):
|
||||
# PUT with custom + known config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21dd%3Atest",
|
||||
'{"visibility":"private","custom":"things"}'
|
||||
)
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_invalid_content(self):
|
||||
# PUT with invalid content / room names, expect 400
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/ee", '{"sdf"'
|
||||
)
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'{"visibili')
|
||||
self.assertEquals(400, code)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/ee", '["hello"]'
|
||||
)
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'["hello"]')
|
||||
self.assertEquals(400, code)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_conflict(self):
|
||||
yield self.create_room_as("!aa:test", self.user_id)
|
||||
|
||||
# PUT with conflicting room ID, expect 409
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21aa%3Atest", "{}"
|
||||
)
|
||||
self.assertEquals(409, code)
|
||||
|
||||
|
||||
class RoomTopicTestCase(RestTestCase):
|
||||
""" Tests /rooms/$room_id/topic REST events. """
|
||||
@ -613,8 +558,6 @@ class RoomTopicTestCase(RestTestCase):
|
||||
def setUp(self):
|
||||
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
|
||||
self.auth_user_id = self.user_id
|
||||
self.room_id = "!rid1:test"
|
||||
self.path = "/rooms/%s/state/m.room.topic" % self.room_id
|
||||
|
||||
state_handler = Mock(spec=["handle_new_event"])
|
||||
state_handler.handle_new_event.return_value = True
|
||||
@ -640,7 +583,8 @@ class RoomTopicTestCase(RestTestCase):
|
||||
synapse.rest.room.register_servlets(hs, self.mock_resource)
|
||||
|
||||
# create the room
|
||||
yield self.create_room_as(self.room_id, self.user_id)
|
||||
self.room_id = yield self.create_room_as(self.user_id)
|
||||
self.path = "/rooms/%s/state/m.room.topic" % self.room_id
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
@ -717,7 +661,6 @@ class RoomMemberStateTestCase(RestTestCase):
|
||||
def setUp(self):
|
||||
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
|
||||
self.auth_user_id = self.user_id
|
||||
self.room_id = "!rid1:test"
|
||||
|
||||
state_handler = Mock(spec=["handle_new_event"])
|
||||
state_handler.handle_new_event.return_value = True
|
||||
@ -742,7 +685,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
||||
|
||||
synapse.rest.room.register_servlets(hs, self.mock_resource)
|
||||
|
||||
yield self.create_room_as(self.room_id, self.user_id)
|
||||
self.room_id = yield self.create_room_as(self.user_id)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
@ -843,7 +786,6 @@ class RoomMessagesTestCase(RestTestCase):
|
||||
def setUp(self):
|
||||
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
|
||||
self.auth_user_id = self.user_id
|
||||
self.room_id = "!rid1:test"
|
||||
|
||||
state_handler = Mock(spec=["handle_new_event"])
|
||||
state_handler.handle_new_event.return_value = True
|
||||
@ -868,7 +810,7 @@ class RoomMessagesTestCase(RestTestCase):
|
||||
|
||||
synapse.rest.room.register_servlets(hs, self.mock_resource)
|
||||
|
||||
yield self.create_room_as(self.room_id, self.user_id)
|
||||
self.room_id = yield self.create_room_as(self.user_id)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
@ -24,6 +24,7 @@ from synapse.api.constants import Membership
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class RestTestCase(unittest.TestCase):
|
||||
"""Contains extra helper functions to quickly and clearly perform a given
|
||||
REST action, which isn't the focus of the test.
|
||||
@ -40,18 +41,19 @@ class RestTestCase(unittest.TestCase):
|
||||
return self.auth_user_id
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def create_room_as(self, room_id, room_creator, is_public=True, tok=None):
|
||||
def create_room_as(self, room_creator, is_public=True, tok=None):
|
||||
temp_id = self.auth_user_id
|
||||
self.auth_user_id = room_creator
|
||||
path = "/rooms/%s" % room_id
|
||||
path = "/createRoom"
|
||||
content = "{}"
|
||||
if not is_public:
|
||||
content = '{"visibility":"private"}'
|
||||
if tok:
|
||||
path = path + "?access_token=%s" % tok
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
||||
(code, response) = yield self.mock_resource.trigger("POST", path, content)
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
self.auth_user_id = temp_id
|
||||
defer.returnValue(response["room_id"])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def invite(self, room=None, src=None, targ=None, expect_code=200, tok=None):
|
||||
|
@ -37,7 +37,11 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
||||
mPresence.start();
|
||||
}
|
||||
|
||||
$scope.go = function(url) {
|
||||
/**
|
||||
* Open a given page.
|
||||
* @param {String} url url of the page
|
||||
*/
|
||||
$scope.goToPage = function(url) {
|
||||
$location.url(url);
|
||||
};
|
||||
|
||||
|
@ -342,6 +342,64 @@ h1 {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/*** Recents ***/
|
||||
.recentsTable {
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.recentsTable tr {
|
||||
width: 100%;
|
||||
}
|
||||
.recentsTable td {
|
||||
vertical-align: text-top;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.recentsRoom {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.recentsRoom:hover {
|
||||
background-color: #f8f8ff;
|
||||
}
|
||||
|
||||
.recentsRoomSelected {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.recentsRoomName {
|
||||
font-size: 16px;
|
||||
padding-top: 7px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.recentsRoomSummaryTS {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
width: 7em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.recentsRoomSummary {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/*** Recents in the room page ***/
|
||||
#roomRecentsTableWrapper {
|
||||
float: left;
|
||||
max-width: 320px;
|
||||
margin-right: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/*** Profile ***/
|
||||
|
||||
.profile-avatar {
|
||||
|
@ -20,6 +20,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
|
||||
'LoginController',
|
||||
'RoomController',
|
||||
'HomeController',
|
||||
'RecentsController',
|
||||
'SettingsController',
|
||||
'UserController',
|
||||
'matrixService',
|
||||
|
@ -97,7 +97,7 @@ angular.module('matrixService', [])
|
||||
// Create a room
|
||||
create: function(room_id, visibility) {
|
||||
// The REST path spec
|
||||
var path = "/rooms";
|
||||
var path = "/createRoom";
|
||||
|
||||
return doRequest("POST", path, undefined, {
|
||||
visibility: visibility,
|
||||
@ -106,11 +106,20 @@ angular.module('matrixService', [])
|
||||
},
|
||||
|
||||
// List all rooms joined or been invited to
|
||||
rooms: function(from, to, limit) {
|
||||
rooms: function(limit, feedback) {
|
||||
// The REST path spec
|
||||
|
||||
var path = "/initialSync";
|
||||
|
||||
return doRequest("GET", path);
|
||||
var params = {};
|
||||
if (limit) {
|
||||
params.limit = limit;
|
||||
}
|
||||
if (feedback) {
|
||||
params.feedback = feedback;
|
||||
}
|
||||
|
||||
return doRequest("GET", path, params);
|
||||
},
|
||||
|
||||
// Joins a room
|
||||
@ -124,7 +133,8 @@ angular.module('matrixService', [])
|
||||
|
||||
path = path.replace("$room_alias", room_alias);
|
||||
|
||||
return doRequest("PUT", path, undefined, {});
|
||||
// TODO: PUT with txn ID
|
||||
return doRequest("POST", path, undefined, {});
|
||||
},
|
||||
|
||||
// Invite a user to a room
|
||||
@ -154,7 +164,7 @@ angular.module('matrixService', [])
|
||||
|
||||
// Retrieves the room ID corresponding to a room alias
|
||||
resolveRoomAlias:function(room_alias) {
|
||||
var path = "/matrix/client/api/v1/ds/room/$room_alias";
|
||||
var path = "/matrix/client/api/v1/directory/room/$room_alias";
|
||||
room_alias = encodeURIComponent(room_alias);
|
||||
|
||||
path = path.replace("$room_alias", room_alias);
|
||||
@ -234,7 +244,7 @@ angular.module('matrixService', [])
|
||||
|
||||
// get a list of public rooms on your home server
|
||||
publicRooms: function() {
|
||||
var path = "/public/rooms"
|
||||
var path = "/publicRooms"
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
@ -405,6 +415,40 @@ angular.module('matrixService', [])
|
||||
config.version = configVersion;
|
||||
localStorage.setItem("config", JSON.stringify(config));
|
||||
},
|
||||
|
||||
|
||||
/****** Room aliases management ******/
|
||||
|
||||
/**
|
||||
* Enhance data returned by rooms() and publicRooms() by adding room_alias
|
||||
* & room_display_name which are computed from data already retrieved from the server.
|
||||
* @param {Array} data the response of rooms() and publicRooms()
|
||||
* @returns {Array} the same array with enriched objects
|
||||
*/
|
||||
assignRoomAliases: function(data) {
|
||||
for (var i=0; i<data.length; i++) {
|
||||
var alias = this.getRoomIdToAliasMapping(data[i].room_id);
|
||||
if (alias) {
|
||||
// use the existing alias from storage
|
||||
data[i].room_alias = alias;
|
||||
data[i].room_display_name = alias;
|
||||
}
|
||||
else if (data[i].aliases && data[i].aliases[0]) {
|
||||
// save the mapping
|
||||
// TODO: select the smarter alias from the array
|
||||
this.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
|
||||
data[i].room_display_name = data[i].aliases[0];
|
||||
}
|
||||
else if (data[i].membership == "invite" && "inviter" in data[i]) {
|
||||
data[i].room_display_name = data[i].inviter + "'s room"
|
||||
}
|
||||
else {
|
||||
// last resort use the room id
|
||||
data[i].room_display_name = data[i].room_id;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
createRoomIdToAliasMapping: function(roomId, alias) {
|
||||
localStorage.setItem(MAPPING_PREFIX+roomId, alias);
|
||||
|
@ -16,12 +16,11 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('HomeController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
|
||||
.controller('HomeController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService', 'eventStreamService',
|
||||
function($scope, $location, matrixService, mFileUpload, eventHandlerService, eventStreamService) {
|
||||
angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
|
||||
.controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService', 'eventStreamService',
|
||||
function($scope, $location, matrixService, eventHandlerService, eventStreamService) {
|
||||
|
||||
$scope.config = matrixService.config();
|
||||
$scope.rooms = {};
|
||||
$scope.public_rooms = [];
|
||||
$scope.newRoomId = "";
|
||||
$scope.feedback = "";
|
||||
@ -32,72 +31,18 @@ angular.module('HomeController', ['matrixService', 'mFileInput', 'mFileUpload',
|
||||
};
|
||||
|
||||
$scope.goToRoom = {
|
||||
room_id: "",
|
||||
room_id: ""
|
||||
};
|
||||
|
||||
$scope.joinAlias = {
|
||||
room_alias: "",
|
||||
};
|
||||
|
||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||
var config = matrixService.config();
|
||||
if (event.state_key === config.user_id && event.content.membership === "invite") {
|
||||
console.log("Invited to room " + event.room_id);
|
||||
// FIXME push membership to top level key to match /im/sync
|
||||
event.membership = event.content.membership;
|
||||
// FIXME bodge a nicer name than the room ID for this invite.
|
||||
event.room_display_name = event.user_id + "'s room";
|
||||
$scope.rooms[event.room_id] = event;
|
||||
}
|
||||
});
|
||||
|
||||
var assignRoomAliases = function(data) {
|
||||
for (var i=0; i<data.length; i++) {
|
||||
var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id);
|
||||
if (alias) {
|
||||
// use the existing alias from storage
|
||||
data[i].room_alias = alias;
|
||||
data[i].room_display_name = alias;
|
||||
}
|
||||
else if (data[i].aliases && data[i].aliases[0]) {
|
||||
// save the mapping
|
||||
// TODO: select the smarter alias from the array
|
||||
matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
|
||||
data[i].room_display_name = data[i].aliases[0];
|
||||
}
|
||||
else if (data[i].membership == "invite" && "inviter" in data[i]) {
|
||||
data[i].room_display_name = data[i].inviter + "'s room"
|
||||
}
|
||||
else {
|
||||
// last resort use the room id
|
||||
data[i].room_display_name = data[i].room_id;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
room_alias: ""
|
||||
};
|
||||
|
||||
var refresh = function() {
|
||||
// List all rooms joined or been invited to
|
||||
matrixService.rooms(1,true).then(
|
||||
function(response) {
|
||||
var data = assignRoomAliases(response.data.rooms);
|
||||
$scope.feedback = "Success";
|
||||
for (var i=0; i<data.length; i++) {
|
||||
$scope.rooms[data[i].room_id] = data[i];
|
||||
}
|
||||
|
||||
var presence = response.data.presence;
|
||||
for (var i = 0; i < presence.length; ++i) {
|
||||
eventHandlerService.handleEvent(presence[i], false);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
});
|
||||
|
||||
matrixService.publicRooms().then(
|
||||
function(response) {
|
||||
$scope.public_rooms = assignRoomAliases(response.data.chunk);
|
||||
$scope.public_rooms = matrixService.assignRoomAliases(response.data.chunk);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -23,13 +23,8 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h3>My rooms</h3>
|
||||
|
||||
<div class="rooms" ng-repeat="(rm_id, room) in rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_display_name }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
|
||||
</div>
|
||||
</div>
|
||||
<h3>Recents</h3>
|
||||
<div ng-include="'recents/recents.html'"></div>
|
||||
<br/>
|
||||
|
||||
<h3>Public rooms</h3>
|
||||
|
@ -19,6 +19,8 @@
|
||||
<script src="app-filter.js"></script>
|
||||
<script src="home/home-controller.js"></script>
|
||||
<script src="login/login-controller.js"></script>
|
||||
<script src="recents/recents-controller.js"></script>
|
||||
<script src="recents/recents-filter.js"></script>
|
||||
<script src="room/room-controller.js"></script>
|
||||
<script src="room/room-directive.js"></script>
|
||||
<script src="settings/settings-controller.js"></script>
|
||||
@ -37,7 +39,7 @@
|
||||
<header id="header">
|
||||
<!-- Do not show buttons on the login page -->
|
||||
<div id="header-buttons" ng-hide="'/login' == location ">
|
||||
<button ng-click='go("settings")'>Settings</button>
|
||||
<button ng-click='goToPage("settings")'>Settings</button>
|
||||
<button ng-click="logout()">Log out</button>
|
||||
</div>
|
||||
</header>
|
||||
|
70
webclient/recents/recents-controller.js
Normal file
70
webclient/recents/recents-controller.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
|
||||
.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService', 'eventStreamService',
|
||||
function($scope, matrixService, eventHandlerService, eventStreamService) {
|
||||
$scope.rooms = {};
|
||||
|
||||
// $scope of the parent where the recents component is included can override this value
|
||||
// in order to highlight a specific room in the list
|
||||
$scope.recentsSelectedRoomID;
|
||||
|
||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||
var config = matrixService.config();
|
||||
if (event.state_key === config.user_id && event.content.membership === "invite") {
|
||||
console.log("Invited to room " + event.room_id);
|
||||
// FIXME push membership to top level key to match /im/sync
|
||||
event.membership = event.content.membership;
|
||||
// FIXME bodge a nicer name than the room ID for this invite.
|
||||
event.room_display_name = event.user_id + "'s room";
|
||||
$scope.rooms[event.room_id] = event;
|
||||
}
|
||||
});
|
||||
|
||||
var refresh = function() {
|
||||
// List all rooms joined or been invited to
|
||||
matrixService.rooms(1, false).then(
|
||||
function(response) {
|
||||
var data = matrixService.assignRoomAliases(response.data.rooms);
|
||||
for (var i=0; i<data.length; i++) {
|
||||
$scope.rooms[data[i].room_id] = data[i];
|
||||
|
||||
// Create a shortcut for the last message of this room
|
||||
if (data[i].messages && data[i].messages.chunk && data[i].messages.chunk[0]) {
|
||||
$scope.rooms[data[i].room_id].lastMsg = data[i].messages.chunk[0];
|
||||
}
|
||||
}
|
||||
|
||||
var presence = response.data.presence;
|
||||
for (var i = 0; i < presence.length; ++i) {
|
||||
eventHandlerService.handleEvent(presence[i], false);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.onInit = function() {
|
||||
refresh();
|
||||
};
|
||||
|
||||
}]);
|
||||
|
47
webclient/recents/recents-filter.js
Normal file
47
webclient/recents/recents-filter.js
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('RecentsController')
|
||||
.filter('orderRecents', function() {
|
||||
return function(rooms) {
|
||||
|
||||
// Transform the dict into an array
|
||||
// The key, room_id, is already in value objects
|
||||
var filtered = [];
|
||||
angular.forEach(rooms, function(value, key) {
|
||||
filtered.push( value );
|
||||
});
|
||||
|
||||
// And time sort them
|
||||
// The room with the lastest message at first
|
||||
filtered.sort(function (a, b) {
|
||||
// Invite message does not have a body message nor ts
|
||||
// Puth them at the top of the list
|
||||
if (undefined === a.lastMsg) {
|
||||
return -1;
|
||||
}
|
||||
else if (undefined === b.lastMsg) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return b.lastMsg.ts - a.lastMsg.ts;
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
};
|
||||
});
|
56
webclient/recents/recents.html
Normal file
56
webclient/recents/recents.html
Normal file
@ -0,0 +1,56 @@
|
||||
<div ng-controller="RecentsController" data-ng-init="onInit()">
|
||||
<table class="recentsTable">
|
||||
<tbody ng-repeat="(rm_id, room) in rooms | orderRecents"
|
||||
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
|
||||
class ="recentsRoom"
|
||||
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
|
||||
<tr>
|
||||
<td class="recentsRoomName">
|
||||
{{ room.room_display_name }}
|
||||
</td>
|
||||
<td class="recentsRoomSummaryTS">
|
||||
{{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" class="recentsRoomSummary">
|
||||
|
||||
<div ng-show="room.membership === 'invite'" >
|
||||
{{ room.inviter }} invited you
|
||||
</div>
|
||||
|
||||
<div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" >
|
||||
<div ng-switch-when="m.room.member">
|
||||
{{ room.lastMsg.user_id }}
|
||||
{{ {"join": "joined", "leave": "left", "invite": "invited"}[room.lastMsg.content.membership] }}
|
||||
{{ room.lastMsg.content.membership === "invite" ? (room.lastMsg.state_key || '') : '' }}
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.room.message">
|
||||
<div ng-switch="room.lastMsg.content.msgtype">
|
||||
<div ng-switch-when="m.text">
|
||||
{{ room.lastMsg.user_id }} :
|
||||
<span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.image">
|
||||
{{ room.lastMsg.user_id }} sent an image
|
||||
</div>
|
||||
|
||||
<div ng-switch-default>
|
||||
{{ room.lastMsg.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-switch-default>
|
||||
{{ room.lastMsg }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -327,6 +327,9 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
var onInit2 = function() {
|
||||
eventHandlerService.reInitRoom($scope.room_id);
|
||||
|
||||
// Make recents highlight the current room
|
||||
$scope.recentsSelectedRoomID = $scope.room_id;
|
||||
|
||||
// Join the room
|
||||
matrixService.join($scope.room_id).then(
|
||||
function() {
|
||||
|
@ -7,7 +7,11 @@
|
||||
<div id="roomName">
|
||||
{{ room_alias || room_id }}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="roomRecentsTableWrapper">
|
||||
<div ng-include="'recents/recents.html'"></div>
|
||||
</div>
|
||||
|
||||
<div id="usersTableWrapper">
|
||||
<table id="usersTable">
|
||||
<tr ng-repeat="member in members | orderMembersList">
|
||||
|
Loading…
Reference in New Issue
Block a user