diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rwxr-xr-x | cmdclient/console.py | 2 | ||||
-rw-r--r-- | docs/client-server/swagger_matrix/api-docs | 4 | ||||
-rw-r--r-- | docs/client-server/swagger_matrix/events | 317 | ||||
-rw-r--r-- | docs/client-server/swagger_matrix/presence | 2 | ||||
-rw-r--r-- | docs/client-server/swagger_matrix/rooms | 259 | ||||
-rw-r--r-- | docs/client-server/urls.rst | 92 | ||||
-rw-r--r-- | synapse/api/auth.py | 2 | ||||
-rwxr-xr-x | synapse/app/homeserver.py | 65 | ||||
-rw-r--r-- | synapse/handlers/__init__.py | 5 | ||||
-rw-r--r-- | synapse/handlers/events.py | 99 | ||||
-rw-r--r-- | synapse/handlers/typing.py | 146 | ||||
-rw-r--r-- | synapse/rest/events.py | 16 | ||||
-rw-r--r-- | synapse/rest/presence.py | 2 | ||||
-rw-r--r-- | synapse/rest/room.py | 94 | ||||
-rw-r--r-- | synapse/server.py | 15 | ||||
-rw-r--r-- | synapse/storage/__init__.py | 1 | ||||
-rw-r--r-- | tests/handlers/test_typing.py | 250 | ||||
-rw-r--r-- | tests/rest/test_events.py | 5 | ||||
-rw-r--r-- | tests/rest/test_presence.py | 6 | ||||
-rw-r--r-- | tests/rest/test_rooms.py | 122 | ||||
-rw-r--r-- | tests/rest/utils.py | 8 | ||||
-rw-r--r-- | webclient/components/matrix/matrix-service.js | 5 |
23 files changed, 771 insertions, 748 deletions
diff --git a/.gitignore b/.gitignore index ab1a3e1aae..c214971e11 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ docs/build/ *.egg-info cmdclient_config.json -homeserver.db +homeserver*.db .coverage htmlcov diff --git a/cmdclient/console.py b/cmdclient/console.py index 4cb604e796..a4d8145d72 100755 --- a/cmdclient/console.py +++ b/cmdclient/console.py @@ -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>" diff --git a/docs/client-server/swagger_matrix/api-docs b/docs/client-server/swagger_matrix/api-docs index d974dbb374..e52d4ee69d 100644 --- a/docs/client-server/swagger_matrix/api-docs +++ b/docs/client-server/swagger_matrix/api-docs @@ -21,6 +21,10 @@ { "path": "/presence", "description": "Presence operations" + }, + { + "path": "/events", + "description": "Event operations" } ], "authorizations": { diff --git a/docs/client-server/swagger_matrix/events b/docs/client-server/swagger_matrix/events index c9eb3f6ff7..e8b96861f5 100644 --- a/docs/client-server/swagger_matrix/events +++ b/docs/client-server/swagger_matrix/events @@ -1,208 +1,59 @@ { "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" - } - ] + "name": "from", + "description": "The token to stream from.", + "required": false, + "type": "string", + "paramType": "query" + }, + { + "name": "timeout", + "description": "The maximum time in milliseconds to wait for an event.", + "required": false, + "type": "integer", + "paramType": "query" } - ] - }, - { - "path": "/user/createWithList", - "operations": [ + ], + "responseMessages": [ { - "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" - } - ] + "code": 400, + "message": "Bad pagination token." } ] }, { - "path": "/user/{username}", + "path": "/events/{eventId}", "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": {}, + "summary": "Get information about a single event.", + "notes": "Get information about a single event.", + "type": "Event", + "nickname": "get_event", "parameters": [ { - "name": "username", - "description": "The name that needs to be fetched. Use user1 for testing.", + "name": "eventId", + "description": "The event ID to get.", "required": true, "type": "string", "paramType": "path" @@ -210,47 +61,8 @@ ], "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" + "message": "Event not found." } ] } @@ -258,42 +70,43 @@ } ], "models": { - "User": { - "id": "User", + "PaginationChunk": { + "id": "PaginationChunk", "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "firstName": { - "type": "string" - }, - "username": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" + "start": { + "type": "string", + "description": "A token which correlates to the first value in \"chunk\" for paginating.", + "required": true }, - "password": { - "type": "string" + "end": { + "type": "string", + "description": "A token which correlates to the last value in \"chunk\" for paginating.", + "required": true }, - "phone": { - "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 }, - "userStatus": { - "type": "integer", - "format": "int32", - "description": "User Status", - "enum": [ - "1-registered", - "2-active", - "3-closed" - ] + "room_id": { + "type": "string", + "description": "The room in which this event occurred.", + "required": true } } } } -} \ No newline at end of file +} diff --git a/docs/client-server/swagger_matrix/presence b/docs/client-server/swagger_matrix/presence index ee9deb12f0..1b4c7323aa 100644 --- a/docs/client-server/swagger_matrix/presence +++ b/docs/client-server/swagger_matrix/presence @@ -55,7 +55,7 @@ ] }, { - "path": "/presence_list/{userId}", + "path": "/presence/list/{userId}", "operations": [ { "method": "GET", diff --git a/docs/client-server/swagger_matrix/rooms b/docs/client-server/swagger_matrix/rooms index 47a8887240..7a2b5399a2 100644 --- a/docs/client-server/swagger_matrix/rooms +++ b/docs/client-server/swagger_matrix/rooms @@ -14,7 +14,7 @@ }, "apis": [ { - "path": "/rooms/{roomId}/messages/{userId}/{messageId}", + "path": "/rooms/{roomId}/send/m.room.message/{txnId}", "operations": [ { "method": "PUT", @@ -41,67 +41,18 @@ "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.", + "name": "txnId", + "description": "A client transaction ID to ensure idempotency.", "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 +78,6 @@ "type": "string", "paramType": "path" } - ], - "responseMessages": [ - { - "code": 403, - "message": "Must send messages as yourself." - } ] }, { @@ -160,7 +105,7 @@ ] }, { - "path": "/rooms/{roomId}/messages/{msgSenderId}/{messageId}/feedback/{senderId}/{feedbackType}", + "path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}", "operations": [ { "method": "PUT", @@ -187,105 +132,33 @@ "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.", + "name": "txnId", + "description": "A client transaction ID to ensure idempotency.", "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": [ @@ -412,22 +285,30 @@ } ] }, + + + + + + + + { - "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 +324,7 @@ ] }, { - "path": "/rooms", + "path": "/createRoom", "operations": [ { "method": "POST", @@ -477,7 +358,7 @@ ] }, { - "path": "/rooms/{roomId}/messages/list", + "path": "/rooms/{roomId}/messages", "operations": [ { "method": "GET", @@ -519,7 +400,7 @@ ] }, { - "path": "/rooms/{roomId}/members/list", + "path": "/rooms/{roomId}/members", "operations": [ { "method": "GET", @@ -732,76 +613,6 @@ "type": "Member" } } - }, - "Tag": { - "id": "Tag", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - } - }, - "Pet": { - "id": "Pet", - "required": [ - "id", - "name" - ], - "properties": { - "id": { - "type": "integer", - "format": "int64", - "description": "unique identifier for the pet", - "minimum": "0.0", - "maximum": "100.0" - }, - "category": { - "$ref": "Category" - }, - "name": { - "type": "string" - }, - "photoUrls": { - "type": "array", - "items": { - "type": "string" - } - }, - "tags": { - "type": "array", - "items": { - "$ref": "Tag" - } - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - } - }, - "Category": { - "id": "Category", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - }, - "pet": { - "$ref": "Pet" - } - } } } } diff --git a/docs/client-server/urls.rst b/docs/client-server/urls.rst deleted file mode 100644 index c0d2eaa80c..0000000000 --- a/docs/client-server/urls.rst +++ /dev/null @@ -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 diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 15407df14a..886e132e10 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -162,6 +162,8 @@ class Auth(object): """ try: user_id = yield self.store.get_user_by_token(token=token) + if not user_id: + raise StoreError() defer.returnValue(self.hs.parse_userid(user_id)) except StoreError: raise AuthError(403, "Unrecognised access token.", diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index f210d26629..a89770ed7b 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -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) - 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", - ] + with sqlite3.connect(self.db_name) as db_conn: + c = db_conn.cursor() + c.execute("PRAGMA user_version") + row = c.fetchone() - for sql_loc in schemas: - sql_script = read_schema(sql_loc) + if row and row[0]: + user_version = row[0] - with sqlite3.connect(self.db_name) as db_conn: - c = db_conn.cursor() - c.executescript(sql_script) - c.close() - db_conn.commit() + 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) + 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): @@ -282,7 +307,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() diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py index 8a4aa6e5d6..b645977767 100644 --- a/synapse/handlers/__init__.py +++ b/synapse/handlers/__init__.py @@ -17,12 +17,13 @@ from .register import RegistrationHandler from .room import ( MessageHandler, RoomCreationHandler, RoomMemberHandler, RoomListHandler ) -from .events import EventStreamHandler +from .events import EventStreamHandler, EventHandler from .federation import FederationHandler from .login import LoginHandler from .profile import ProfileHandler from .presence import PresenceHandler from .directory import DirectoryHandler +from .typing import TypingNotificationHandler class Handlers(object): @@ -39,9 +40,11 @@ class Handlers(object): self.room_creation_handler = RoomCreationHandler(hs) self.room_member_handler = RoomMemberHandler(hs) self.event_stream_handler = EventStreamHandler(hs) + self.event_handler = EventHandler(hs) self.federation_handler = FederationHandler(hs) self.profile_handler = ProfileHandler(hs) self.presence_handler = PresenceHandler(hs) self.room_list_handler = RoomListHandler(hs) self.login_handler = LoginHandler(hs) self.directory_handler = DirectoryHandler(hs) + self.typing_notification_handler = TypingNotificationHandler(hs) diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index aabec37fc0..b336b292d3 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -47,26 +47,81 @@ class EventStreamHandler(BaseHandler): def get_stream(self, auth_user_id, pagin_config, timeout=0): auth_user = self.hs.parse_userid(auth_user_id) - if pagin_config.from_token is None: - pagin_config.from_token = None - - rm_handler = self.hs.get_handlers().room_member_handler - room_ids = yield rm_handler.get_rooms_for_user(auth_user) - - events, tokens = yield self.notifier.get_events_for( - auth_user, room_ids, pagin_config, timeout - ) - - chunks = [ - e.get_dict() if isinstance(e, SynapseEvent) else e - for e in events - ] - - chunk = { - "chunk": chunks, - "start": tokens[0].to_string(), - "end": tokens[1].to_string(), - } - - defer.returnValue(chunk) + try: + if auth_user not in self._streams_per_user: + self._streams_per_user[auth_user] = 0 + if auth_user in self._stop_timer_per_user: + self.clock.cancel_call_later( + self._stop_timer_per_user.pop(auth_user)) + else: + self.distributor.fire( + "started_user_eventstream", auth_user + ) + self._streams_per_user[auth_user] += 1 + + + if pagin_config.from_token is None: + pagin_config.from_token = None + + rm_handler = self.hs.get_handlers().room_member_handler + room_ids = yield rm_handler.get_rooms_for_user(auth_user) + + events, tokens = yield self.notifier.get_events_for( + auth_user, room_ids, pagin_config, timeout + ) + + chunks = [ + e.get_dict() if isinstance(e, SynapseEvent) else e + for e in events + ] + + chunk = { + "chunk": chunks, + "start": tokens[0].to_string(), + "end": tokens[1].to_string(), + } + + defer.returnValue(chunk) + + finally: + self._streams_per_user[auth_user] -= 1 + if not self._streams_per_user[auth_user]: + del self._streams_per_user[auth_user] + + # 10 seconds of grace to allow the client to reconnect again + # before we think they're gone + def _later(): + self.distributor.fire( + "stopped_user_eventstream", auth_user + ) + del self._stop_timer_per_user[auth_user] + + self._stop_timer_per_user[auth_user] = ( + self.clock.call_later(5, _later) + ) + + +class EventHandler(BaseHandler): + @defer.inlineCallbacks + def get_event(self, user, event_id): + """Retrieve a single specified event. + + Args: + user (synapse.types.UserID): The user requesting the event + event_id (str): The event ID to obtain. + Returns: + dict: An event, or None if there is no event matching this ID. + Raises: + SynapseError if there was a problem retrieving this event, or + AuthError if the user does not have the rights to inspect this + event. + """ + event = yield self.store.get_event(event_id) + + if not event: + defer.returnValue(None) + return + + yield self.auth.check(event, raises=True) + defer.returnValue(event) diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py new file mode 100644 index 0000000000..9d38a7336e --- /dev/null +++ b/synapse/handlers/typing.py @@ -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 diff --git a/synapse/rest/events.py b/synapse/rest/events.py index 28da418498..2e7563d14b 100644 --- a/synapse/rest/events.py +++ b/synapse/rest/events.py @@ -48,6 +48,22 @@ class EventStreamRestServlet(RestServlet): return (200, {}) +# TODO: Unit test gets, with and without auth, with different kinds of events. +class EventRestServlet(RestServlet): + PATTERN = client_path_pattern("/events/(?P<event_id>[^/]*)$") + + @defer.inlineCallbacks + def on_GET(self, request, event_id): + auth_user = yield self.auth.get_user_by_req(request) + handler = self.handlers.event_handler + event = yield handler.get_event(auth_user, event_id) + + if event: + defer.returnValue((200, event.get_dict())) + else: + defer.returnValue((404, "Event not found.")) + def register_servlets(hs, http_server): EventStreamRestServlet(hs).register(http_server) + EventRestServlet(hs).register(http_server) diff --git a/synapse/rest/presence.py b/synapse/rest/presence.py index 6043848595..e013b20853 100644 --- a/synapse/rest/presence.py +++ b/synapse/rest/presence.py @@ -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): diff --git a/synapse/rest/room.py b/synapse/rest/room.py index 9363acebed..f4c12191c8 100644 --- a/synapse/rest/room.py +++ b/synapse/rest/room.py @@ -18,11 +18,9 @@ from twisted.internet import defer from base import RestServlet, client_path_pattern from synapse.api.errors import SynapseError, Codes -from synapse.api.events.room import ( - MessageEvent, RoomMemberEvent, FeedbackEvent -) -from synapse.api.constants import Feedback from synapse.streams.config import PaginationConfig +from synapse.api.events.room import RoomMemberEvent +from synapse.api.constants import Membership import json import logging @@ -36,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): @@ -210,24 +205,63 @@ class RoomSendEventRestServlet(RestServlet): defer.returnValue(response) +# TODO: Needs unit testing for room ID + alias joins class JoinRoomAliasServlet(RestServlet): - PATTERN = client_path_pattern("/join/(?P<room_alias>[^/]+)$") + + def register(self, http_server): + # /join/$room_identifier[/$txn_id] + PATTERN = ("/join/(?P<room_identifier>[^/]*)") + register_txn_path(self, PATTERN, http_server) @defer.inlineCallbacks - def on_PUT(self, request, room_alias): + def on_POST(self, request, room_identifier): user = yield self.auth.get_user_by_req(request) - if not user: - defer.returnValue((403, "Unrecognized user")) + # the identifier could be a room alias or a room id. Try one then the + # other if it fails to parse, without swallowing other valid + # SynapseErrors. - logger.debug("room_alias: %s", room_alias) + identifier = None + is_room_alias = False + try: + identifier = self.hs.parse_roomalias( + urllib.unquote(room_identifier) + ) + is_room_alias = True + except SynapseError: + identifier = self.hs.parse_roomid( + urllib.unquote(room_identifier) + ) - room_alias = self.hs.parse_roomalias(urllib.unquote(room_alias)) + # TODO: Support for specifying the home server to join with? - handler = self.handlers.room_member_handler - ret_dict = yield handler.join_room_alias(user, room_alias) + if is_room_alias: + handler = self.handlers.room_member_handler + ret_dict = yield handler.join_room_alias(user, identifier) + defer.returnValue((200, ret_dict)) + else: # room id + event = self.event_factory.create_event( + etype=RoomMemberEvent.TYPE, + content={"membership": Membership.JOIN}, + room_id=urllib.unquote(identifier.to_string()), + user_id=user.to_string(), + state_key=user.to_string() + ) + handler = self.handlers.room_member_handler + yield handler.change_membership(event) + defer.returnValue((200, "")) - defer.returnValue((200, ret_dict)) + @defer.inlineCallbacks + def on_PUT(self, request, room_identifier, txn_id): + try: + defer.returnValue(self.txns.get_client_transaction(request, txn_id)) + except KeyError: + pass + + response = yield self.on_POST(request, room_identifier) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) # TODO: Needs unit testing diff --git a/synapse/server.py b/synapse/server.py index 5e9b76603a..c29c61220d 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -28,7 +28,7 @@ from synapse.handlers import Handlers from synapse.rest import RestServletFactory from synapse.state import StateHandler from synapse.storage import DataStore -from synapse.types import UserID, RoomAlias +from synapse.types import UserID, RoomAlias, RoomID from synapse.util import Clock from synapse.util.distributor import Distributor from synapse.util.lockutils import LockManager @@ -119,17 +119,30 @@ class BaseHomeServer(object): setattr(BaseHomeServer, "get_%s" % (depname), _get) + # TODO: Why are these parse_ methods so high up along with other globals? + # Surely these should be in a util package or in the api package? + # Other utility methods def parse_userid(self, s): """Parse the string given by 's' as a User ID and return a UserID 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.""" return RoomAlias.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) + # Build magic accessors for every dependency for depname in BaseHomeServer.DEPENDENCIES: BaseHomeServer._make_dependency_method(depname) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index a97a42e1e3..38ab03c45c 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -80,7 +80,6 @@ class DataStore(RoomMemberStore, RoomStore, [ "event_id", "type", - "sender", "room_id", "content", "unrecognized_keys" diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py new file mode 100644 index 0000000000..300a6e340a --- /dev/null +++ b/tests/handlers/test_typing.py @@ -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() diff --git a/tests/rest/test_events.py b/tests/rest/test_events.py index 7bc05dc2b6..94ad8910e3 100644 --- a/tests/rest/test_events.py +++ b/tests/rest/test_events.py @@ -178,9 +178,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) diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py index 970405d271..e249a0d48a 100644 --- a/tests/rest/test_presence.py +++ b/tests/rest/test_presence.py @@ -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"]}""" ) diff --git a/tests/rest/test_rooms.py b/tests/rest/test_rooms.py index f18c506a7d..589b434446 100644 --- a/tests/rest/test_rooms.py +++ b/tests/rest/test_rooms.py @@ -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 diff --git a/tests/rest/utils.py b/tests/rest/utils.py index 590d12f155..ef9a6071e2 100644 --- a/tests/rest/utils.py +++ b/tests/rest/utils.py @@ -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): diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 2286485605..e467ca40da 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -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, @@ -124,7 +124,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 |