From dbe71e670c9f7068591b61f0975fdf5225a2cf3f Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Wed, 21 Jan 2015 16:58:16 +0000 Subject: Use common base class for two Presence unit-tests, avoiding boilerplate copypasta --- tests/handlers/test_presence.py | 79 +++++++++++++---------------------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index b85a89052a..e96b73f977 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -59,23 +59,29 @@ class JustPresenceHandlers(object): def __init__(self, hs): self.presence_handler = PresenceHandler(hs) -class PresenceStateTestCase(unittest.TestCase): - """ Tests presence management. """ +class PresenceTestCase(unittest.TestCase): @defer.inlineCallbacks def setUp(self): db_pool = SQLiteMemoryDbPool() yield db_pool.prepare() + self.clock = MockClock() + self.mock_config = NonCallableMock() self.mock_config.signing_key = [MockKey()] + self.mock_federation_resource = MockHttpResource() + + self.mock_http_client = Mock(spec=[]) + self.mock_http_client.put_json = DeferredMockCallable() + hs = HomeServer("test", - clock=MockClock(), + clock=self.clock, db_pool=db_pool, handlers=None, - resource_for_federation=Mock(), - http_client=None, + resource_for_federation=self.mock_federation_resource, + http_client=self.mock_http_client, config=self.mock_config, keyring=Mock(), ) @@ -92,11 +98,19 @@ class PresenceStateTestCase(unittest.TestCase): self.u_banana = hs.parse_userid("@banana:test") self.u_clementine = hs.parse_userid("@clementine:test") - yield self.store.create_presence(self.u_apple.localpart) + for u in self.u_apple, self.u_banana, self.u_clementine: + yield self.store.create_presence(u.localpart) + yield self.store.set_presence_state( self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"} ) + # ID of a local user that does not exist + self.u_durian = hs.parse_userid("@durian:test") + + # A remote user + self.u_cabbage = hs.parse_userid("@cabbage:elsewhere") + self.handler = hs.get_handlers().presence_handler self.room_members = [] @@ -128,6 +142,10 @@ class PresenceStateTestCase(unittest.TestCase): self.handler.start_polling_presence = self.mock_start self.handler.stop_polling_presence = self.mock_stop + +class PresenceStateTestCase(PresenceTestCase): + """ Tests presence management. """ + @defer.inlineCallbacks def test_get_my_state(self): state = yield self.handler.get_state( @@ -206,56 +224,9 @@ class PresenceStateTestCase(unittest.TestCase): self.mock_stop.assert_called_with(self.u_apple) -class PresenceInvitesTestCase(unittest.TestCase): +class PresenceInvitesTestCase(PresenceTestCase): """ Tests presence management. """ - @defer.inlineCallbacks - def setUp(self): - self.mock_http_client = Mock(spec=[]) - self.mock_http_client.put_json = DeferredMockCallable() - - self.mock_federation_resource = MockHttpResource() - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - hs = HomeServer("test", - clock=MockClock(), - db_pool=db_pool, - handlers=None, - resource_for_client=Mock(), - resource_for_federation=self.mock_federation_resource, - http_client=self.mock_http_client, - config=self.mock_config, - keyring=Mock(), - ) - hs.handlers = JustPresenceHandlers(hs) - - self.store = hs.get_datastore() - - # Some local users to test with - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - yield self.store.create_presence(self.u_apple.localpart) - yield self.store.create_presence(self.u_banana.localpart) - - # ID of a local user that does not exist - self.u_durian = hs.parse_userid("@durian:test") - - # A remote user - self.u_cabbage = hs.parse_userid("@cabbage:elsewhere") - - self.handler = hs.get_handlers().presence_handler - - self.mock_start = Mock() - self.mock_stop = Mock() - - self.handler.start_polling_presence = self.mock_start - self.handler.stop_polling_presence = self.mock_stop - @defer.inlineCallbacks def test_invite_local(self): # TODO(paul): This test will likely break if/when real auth permissions -- cgit 1.5.1 From 73315ce9de1612ad936b490fe164d9eb61a51b34 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Wed, 21 Jan 2015 20:01:57 +0000 Subject: Abstract out the room ID from presence tests, so it's stored in self --- tests/handlers/test_presence.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index e96b73f977..c309fbb054 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -113,17 +113,18 @@ class PresenceTestCase(unittest.TestCase): self.handler = hs.get_handlers().presence_handler + self.room_id = "a-room" self.room_members = [] def get_rooms_for_user(user): if user in self.room_members: - return defer.succeed(["a-room"]) + return defer.succeed([self.room_id]) else: return defer.succeed([]) room_member_handler.get_rooms_for_user = get_rooms_for_user def get_room_members(room_id): - if room_id == "a-room": + if room_id == self.room_id: return defer.succeed(self.room_members) else: return defer.succeed([]) @@ -529,24 +530,25 @@ class PresencePushTestCase(unittest.TestCase): ]) self.room_member_handler = hs.handlers.room_member_handler + self.room_id = "a-room" self.room_members = [] def get_rooms_for_user(user): if user in self.room_members: - return defer.succeed(["a-room"]) + 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 == "a-room": + 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 def get_room_hosts(room_id): - if room_id == "a-room": + if room_id == self.room_id: hosts = set([u.domain for u in self.room_members]) return defer.succeed(hosts) else: @@ -882,7 +884,7 @@ class PresencePushTestCase(unittest.TestCase): ) yield self.distributor.fire("user_joined_room", self.u_clementine, - "a-room" + self.room_id ) self.room_members.append(self.u_clementine) @@ -945,7 +947,7 @@ class PresencePushTestCase(unittest.TestCase): self.room_members = [self.u_apple, self.u_banana] yield self.distributor.fire("user_joined_room", self.u_potato, - "a-room" + self.room_id ) yield put_json.await_calls() @@ -974,7 +976,7 @@ class PresencePushTestCase(unittest.TestCase): self.room_members.append(self.u_potato) yield self.distributor.fire("user_joined_room", self.u_clementine, - "a-room" + self.room_id ) put_json.await_calls() -- cgit 1.5.1 From f4ce61ed36e7b639550953882b6ea14171108784 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 15 Jan 2015 16:57:00 +0000 Subject: Move scripts into scripts --- UPGRADE.rst | 4 ++-- database-prepare-for-0.0.1.sh | 21 --------------------- database-prepare-for-0.5.0.sh | 21 --------------------- database-save.sh | 16 ---------------- nuke-room-from-db.sh | 24 ------------------------ scripts/database-prepare-for-0.0.1.sh | 21 +++++++++++++++++++++ scripts/database-prepare-for-0.5.0.sh | 21 +++++++++++++++++++++ scripts/database-save.sh | 16 ++++++++++++++++ scripts/nuke-room-from-db.sh | 24 ++++++++++++++++++++++++ scripts/sphinx_api_docs.sh | 1 + sphinx_api_docs.sh | 1 - 11 files changed, 85 insertions(+), 85 deletions(-) delete mode 100755 database-prepare-for-0.0.1.sh delete mode 100755 database-prepare-for-0.5.0.sh delete mode 100755 database-save.sh delete mode 100755 nuke-room-from-db.sh create mode 100755 scripts/database-prepare-for-0.0.1.sh create mode 100755 scripts/database-prepare-for-0.5.0.sh create mode 100755 scripts/database-save.sh create mode 100755 scripts/nuke-room-from-db.sh create mode 100644 scripts/sphinx_api_docs.sh delete mode 100644 sphinx_api_docs.sh diff --git a/UPGRADE.rst b/UPGRADE.rst index 9618ad2d57..0f81f3e11f 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -52,7 +52,7 @@ resulting conflicts during the upgrade process. Before running the command the homeserver should be first completely shutdown. To run it, simply specify the location of the database, e.g.: - ./database-prepare-for-0.5.0.sh "homeserver.db" + ./scripts/database-prepare-for-0.5.0.sh "homeserver.db" Once this has successfully completed it will be safe to restart the homeserver. You may notice that the homeserver takes a few seconds longer to @@ -147,7 +147,7 @@ rooms the home server was a member of and room alias mappings. Before running the command the homeserver should be first completely shutdown. To run it, simply specify the location of the database, e.g.: - ./database-prepare-for-0.0.1.sh "homeserver.db" + ./scripts/database-prepare-for-0.0.1.sh "homeserver.db" Once this has successfully completed it will be safe to restart the homeserver. You may notice that the homeserver takes a few seconds longer to diff --git a/database-prepare-for-0.0.1.sh b/database-prepare-for-0.0.1.sh deleted file mode 100755 index 43d759a5cd..0000000000 --- a/database-prepare-for-0.0.1.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# This is will prepare a synapse database for running with v0.0.1 of synapse. -# It will store all the user information, but will *delete* all messages and -# room data. - -set -e - -cp "$1" "$1.bak" - -DUMP=$(sqlite3 "$1" << 'EOF' -.dump users -.dump access_tokens -.dump presence -.dump profiles -EOF -) - -rm "$1" - -sqlite3 "$1" <<< "$DUMP" diff --git a/database-prepare-for-0.5.0.sh b/database-prepare-for-0.5.0.sh deleted file mode 100755 index e824cb583e..0000000000 --- a/database-prepare-for-0.5.0.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# This is will prepare a synapse database for running with v0.5.0 of synapse. -# It will store all the user information, but will *delete* all messages and -# room data. - -set -e - -cp "$1" "$1.bak" - -DUMP=$(sqlite3 "$1" << 'EOF' -.dump users -.dump access_tokens -.dump presence -.dump profiles -EOF -) - -rm "$1" - -sqlite3 "$1" <<< "$DUMP" diff --git a/database-save.sh b/database-save.sh deleted file mode 100755 index 040c8a4943..0000000000 --- a/database-save.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -# This script will write a dump file of local user state if you want to splat -# your entire server database and start again but preserve the identity of -# local users and their access tokens. -# -# To restore it, use -# -# $ sqlite3 homeserver.db < table-save.sql - -sqlite3 "$1" <<'EOF' >table-save.sql -.dump users -.dump access_tokens -.dump presence -.dump profiles -EOF diff --git a/nuke-room-from-db.sh b/nuke-room-from-db.sh deleted file mode 100755 index 58c036c896..0000000000 --- a/nuke-room-from-db.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -## CAUTION: -## This script will remove (hopefully) all trace of the given room ID from -## your homeserver.db - -## Do not run it lightly. - -ROOMID="$1" - -sqlite3 homeserver.db <table-save.sql +.dump users +.dump access_tokens +.dump presence +.dump profiles +EOF diff --git a/scripts/nuke-room-from-db.sh b/scripts/nuke-room-from-db.sh new file mode 100755 index 0000000000..58c036c896 --- /dev/null +++ b/scripts/nuke-room-from-db.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +## CAUTION: +## This script will remove (hopefully) all trace of the given room ID from +## your homeserver.db + +## Do not run it lightly. + +ROOMID="$1" + +sqlite3 homeserver.db < Date: Thu, 15 Jan 2015 16:51:52 +0000 Subject: Remove jsfiddles --- jsfiddles/create_room_send_msg/demo.css | 17 -- jsfiddles/create_room_send_msg/demo.html | 30 --- jsfiddles/create_room_send_msg/demo.js | 113 ----------- jsfiddles/event_stream/demo.css | 17 -- jsfiddles/event_stream/demo.html | 23 --- jsfiddles/event_stream/demo.js | 145 -------------- jsfiddles/example_app/demo.css | 43 ---- jsfiddles/example_app/demo.details | 7 - jsfiddles/example_app/demo.html | 56 ------ jsfiddles/example_app/demo.js | 327 ------------------------------- jsfiddles/register_login/demo.css | 7 - jsfiddles/register_login/demo.html | 20 -- jsfiddles/register_login/demo.js | 79 -------- jsfiddles/room_memberships/demo.css | 17 -- jsfiddles/room_memberships/demo.html | 37 ---- jsfiddles/room_memberships/demo.js | 141 ------------- 16 files changed, 1079 deletions(-) delete mode 100644 jsfiddles/create_room_send_msg/demo.css delete mode 100644 jsfiddles/create_room_send_msg/demo.html delete mode 100644 jsfiddles/create_room_send_msg/demo.js delete mode 100644 jsfiddles/event_stream/demo.css delete mode 100644 jsfiddles/event_stream/demo.html delete mode 100644 jsfiddles/event_stream/demo.js delete mode 100644 jsfiddles/example_app/demo.css delete mode 100644 jsfiddles/example_app/demo.details delete mode 100644 jsfiddles/example_app/demo.html delete mode 100644 jsfiddles/example_app/demo.js delete mode 100644 jsfiddles/register_login/demo.css delete mode 100644 jsfiddles/register_login/demo.html delete mode 100644 jsfiddles/register_login/demo.js delete mode 100644 jsfiddles/room_memberships/demo.css delete mode 100644 jsfiddles/room_memberships/demo.html delete mode 100644 jsfiddles/room_memberships/demo.js diff --git a/jsfiddles/create_room_send_msg/demo.css b/jsfiddles/create_room_send_msg/demo.css deleted file mode 100644 index 48a55f372d..0000000000 --- a/jsfiddles/create_room_send_msg/demo.css +++ /dev/null @@ -1,17 +0,0 @@ -.loggedin { - visibility: hidden; -} - -p { - font-family: monospace; -} - -table -{ - border-spacing:5px; -} - -th,td -{ - padding:5px; -} diff --git a/jsfiddles/create_room_send_msg/demo.html b/jsfiddles/create_room_send_msg/demo.html deleted file mode 100644 index 088ff7ac0f..0000000000 --- a/jsfiddles/create_room_send_msg/demo.html +++ /dev/null @@ -1,30 +0,0 @@ -
-

This room creation / message sending demo requires a home server to be running on http://localhost:8008

-
-
- - - -
-
-
- - -
-
- - - -
- - - - - - - - - -
Room IDMy stateRoom AliasLatest message
-
- diff --git a/jsfiddles/create_room_send_msg/demo.js b/jsfiddles/create_room_send_msg/demo.js deleted file mode 100644 index 9c346e2f64..0000000000 --- a/jsfiddles/create_room_send_msg/demo.js +++ /dev/null @@ -1,113 +0,0 @@ -var accountInfo = {}; - -var showLoggedIn = function(data) { - accountInfo = data; - getCurrentRoomList(); - $(".loggedin").css({visibility: "visible"}); -}; - -$('.login').live('click', function() { - var user = $("#userLogin").val(); - var password = $("#passwordLogin").val(); - $.ajax({ - url: "http://localhost:8008/_matrix/client/api/v1/login", - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), - dataType: "json", - success: function(data) { - showLoggedIn(data); - }, - error: function(err) { - var errMsg = "To try this, you need a home server running!"; - var errJson = $.parseJSON(err.responseText); - if (errJson) { - errMsg = JSON.stringify(errJson); - } - alert(errMsg); - } - }); -}); - -var getCurrentRoomList = function() { - var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; - $.getJSON(url, function(data) { - var rooms = data.rooms; - for (var i=0; i 0) { - data.room_alias_name = roomAlias; - } - $.ajax({ - url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify(data), - dataType: "json", - success: function(data) { - data.membership = "join"; // you are automatically joined into every room you make. - data.latest_message = ""; - addRoom(data); - }, - error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); - } - }); -}); - -var addRoom = function(data) { - row = "" + - ""+data.room_id+"" + - ""+data.membership+"" + - ""+data.room_alias+"" + - ""+data.latest_message+"" + - ""; - $("#rooms").append(row); -}; - -$('.sendMessage').live('click', function() { - var roomId = $("#roomId").val(); - var body = $("#messageBody").val(); - var msgId = $.now(); - - if (roomId.length === 0 || body.length === 0) { - return; - } - - var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; - url = url.replace("$token", accountInfo.access_token); - url = url.replace("$roomid", encodeURIComponent(roomId)); - - var data = { - msgtype: "m.text", - body: body - }; - - $.ajax({ - url: url, - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify(data), - dataType: "json", - success: function(data) { - $("#messageBody").val(""); - // wipe the table and reload it. Using the event stream would be the best - // solution but that is out of scope of this fiddle. - $("#rooms").find("tr:gt(0)").remove(); - getCurrentRoomList(); - }, - error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); - } - }); -}); diff --git a/jsfiddles/event_stream/demo.css b/jsfiddles/event_stream/demo.css deleted file mode 100644 index 48a55f372d..0000000000 --- a/jsfiddles/event_stream/demo.css +++ /dev/null @@ -1,17 +0,0 @@ -.loggedin { - visibility: hidden; -} - -p { - font-family: monospace; -} - -table -{ - border-spacing:5px; -} - -th,td -{ - padding:5px; -} diff --git a/jsfiddles/event_stream/demo.html b/jsfiddles/event_stream/demo.html deleted file mode 100644 index 7657780d28..0000000000 --- a/jsfiddles/event_stream/demo.html +++ /dev/null @@ -1,23 +0,0 @@ -
-

This event stream demo requires a home server to be running on http://localhost:8008

-
-
- - - -
-
-
- -
-

- - - - - - - -
Room IDLatest message
-
- diff --git a/jsfiddles/event_stream/demo.js b/jsfiddles/event_stream/demo.js deleted file mode 100644 index acba8391fa..0000000000 --- a/jsfiddles/event_stream/demo.js +++ /dev/null @@ -1,145 +0,0 @@ -var accountInfo = {}; - -var eventStreamInfo = { - from: "END" -}; - -var roomInfo = []; - -var longpollEventStream = function() { - var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from"; - url = url.replace("$token", accountInfo.access_token); - url = url.replace("$from", eventStreamInfo.from); - - $.getJSON(url, function(data) { - eventStreamInfo.from = data.end; - - var hasNewLatestMessage = false; - for (var i=0; i"+roomList[i].room_id+"" + - ""+roomList[i].latest_message+"" + - ""; - rows += row; - } - - $("#rooms").append(rows); -}; - diff --git a/jsfiddles/example_app/demo.css b/jsfiddles/example_app/demo.css deleted file mode 100644 index 4c1e157cc8..0000000000 --- a/jsfiddles/example_app/demo.css +++ /dev/null @@ -1,43 +0,0 @@ -.roomListDashboard, .roomContents, .sendMessageForm { - visibility: hidden; -} - -.roomList { - background-color: #909090; -} - -.messageWrapper { - background-color: #EEEEEE; - height: 400px; - overflow: scroll; -} - -.membersWrapper { - background-color: #EEEEEE; - height: 200px; - width: 50%; - overflow: scroll; -} - -.textEntry { - width: 100% -} - -p { - font-family: monospace; -} - -table -{ - border-spacing:5px; -} - -th,td -{ - padding:5px; -} - -.roomList tr:not(:first-child):hover { - background-color: orange; - cursor: pointer; -} diff --git a/jsfiddles/example_app/demo.details b/jsfiddles/example_app/demo.details deleted file mode 100644 index 3f96d3e744..0000000000 --- a/jsfiddles/example_app/demo.details +++ /dev/null @@ -1,7 +0,0 @@ - name: Example Matrix Client - description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists. - authors: - - matrix.org - resources: - - http://matrix.org - normalize_css: no \ No newline at end of file diff --git a/jsfiddles/example_app/demo.html b/jsfiddles/example_app/demo.html deleted file mode 100644 index 7a9dffddd0..0000000000 --- a/jsfiddles/example_app/demo.html +++ /dev/null @@ -1,56 +0,0 @@ - - -
-
- - -
- - - - - - - - -
RoomMy stateLatest message
-
- -
-

Select a room

-
- - - -
-
-
- - -
-
- -
-

Member list:

-
- - - -
-
-
- diff --git a/jsfiddles/example_app/demo.js b/jsfiddles/example_app/demo.js deleted file mode 100644 index 13c9c2b339..0000000000 --- a/jsfiddles/example_app/demo.js +++ /dev/null @@ -1,327 +0,0 @@ -var accountInfo = {}; - -var eventStreamInfo = { - from: "END" -}; - -var roomInfo = []; -var memberInfo = []; -var viewingRoomId; - -// ************** Event Streaming ************** -var longpollEventStream = function() { - var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from"; - url = url.replace("$token", accountInfo.access_token); - url = url.replace("$from", eventStreamInfo.from); - - $.getJSON(url, function(data) { - eventStreamInfo.from = data.end; - - var hasNewLatestMessage = false; - var updatedMemberList = false; - var i=0; - var j=0; - for (i=0; i 0) { - data.room_alias_name = roomAlias; - } - $.ajax({ - url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify(data), - dataType: "json", - success: function(response) { - $("#roomAlias").val(""); - response.membership = "join"; // you are automatically joined into every room you make. - response.latest_message = ""; - - roomInfo.push(response); - setRooms(roomInfo); - }, - error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); - } - }); -}); - -// ************** Getting current state ************** -var getCurrentRoomList = function() { - var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; - $.getJSON(url, function(data) { - var rooms = data.rooms; - for (var i=0; i=0; --i) { - addMessage(data.chunk[i]); - } - }); -}; - -var getMemberList = function(roomId) { - $("#members").empty(); - memberInfo = []; - var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" + - encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token; - $.getJSON(url, function(data) { - for (var i=0; i"+roomList[i].room_id+"" + - ""+roomList[i].membership+"" + - ""+roomList[i].latest_message+"" + - ""; - rows += row; - } - - $("#rooms").append(rows); - - $('#rooms').find("tr").click(function(){ - var roomId = $(this).find('td:eq(0)').text(); - var membership = $(this).find('td:eq(1)').text(); - if (membership !== "join") { - console.log("Joining room " + roomId); - var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token"; - url = url.replace("$token", accountInfo.access_token); - url = url.replace("$roomid", encodeURIComponent(roomId)); - $.ajax({ - url: url, - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify({membership: "join"}), - dataType: "json", - success: function(data) { - loadRoomContent(roomId); - getCurrentRoomList(); - }, - error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); - } - }); - } - else { - loadRoomContent(roomId); - } - }); -}; - -var addMessage = function(data) { - - var msg = data.content.body; - if (data.type === "m.room.member") { - if (data.content.membership === undefined) { - return; - } - if (data.content.membership === "invite") { - msg = "invited " + data.state_key + " to the room"; - } - else if (data.content.membership === "join") { - msg = "joined the room"; - } - else if (data.content.membership === "leave") { - msg = "left the room"; - } - else if (data.content.membership === "ban") { - msg = "was banned from the room"; - } - } - if (msg === undefined) { - return; - } - - var row = "" + - ""+data.user_id+"" + - ""+msg+"" + - ""; - $("#messages").append(row); -}; - -var addMember = function(data) { - var row = "" + - ""+data.state_key+"" + - ""+data.content.membership+"" + - ""; - $("#members").append(row); -}; - diff --git a/jsfiddles/register_login/demo.css b/jsfiddles/register_login/demo.css deleted file mode 100644 index 11781c250f..0000000000 --- a/jsfiddles/register_login/demo.css +++ /dev/null @@ -1,7 +0,0 @@ -.loggedin { - visibility: hidden; -} - -p { - font-family: monospace; -} diff --git a/jsfiddles/register_login/demo.html b/jsfiddles/register_login/demo.html deleted file mode 100644 index fcac453ac2..0000000000 --- a/jsfiddles/register_login/demo.html +++ /dev/null @@ -1,20 +0,0 @@ -
-

This registration/login demo requires a home server to be running on http://localhost:8008

-
-
- - - -
-
- - - -
-
-

- - -

-
- diff --git a/jsfiddles/register_login/demo.js b/jsfiddles/register_login/demo.js deleted file mode 100644 index 2e6957b631..0000000000 --- a/jsfiddles/register_login/demo.js +++ /dev/null @@ -1,79 +0,0 @@ -var accountInfo = {}; - -var showLoggedIn = function(data) { - accountInfo = data; - $(".loggedin").css({visibility: "visible"}); - $("#welcomeText").text("Welcome " + accountInfo.user_id+". Your access token is: " + - accountInfo.access_token); -}; - -$('.register').live('click', function() { - var user = $("#user").val(); - var password = $("#password").val(); - $.ajax({ - url: "http://localhost:8008/_matrix/client/api/v1/register", - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), - dataType: "json", - success: function(data) { - showLoggedIn(data); - }, - error: function(err) { - var errMsg = "To try this, you need a home server running!"; - var errJson = $.parseJSON(err.responseText); - if (errJson) { - errMsg = JSON.stringify(errJson); - } - alert(errMsg); - } - }); -}); - -var login = function(user, password) { - $.ajax({ - url: "http://localhost:8008/_matrix/client/api/v1/login", - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), - dataType: "json", - success: function(data) { - showLoggedIn(data); - }, - error: function(err) { - var errMsg = "To try this, you need a home server running!"; - var errJson = $.parseJSON(err.responseText); - if (errJson) { - errMsg = JSON.stringify(errJson); - } - alert(errMsg); - } - }); -}; - -$('.login').live('click', function() { - var user = $("#userLogin").val(); - var password = $("#passwordLogin").val(); - $.getJSON("http://localhost:8008/_matrix/client/api/v1/login", function(data) { - if (data.flows[0].type !== "m.login.password") { - alert("I don't know how to login with this type: " + data.type); - return; - } - login(user, password); - }); -}); - -$('.logout').live('click', function() { - accountInfo = {}; - $("#imSyncText").text(""); - $(".loggedin").css({visibility: "hidden"}); -}); - -$('.testToken').live('click', function() { - var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; - $.getJSON(url, function(data) { - $("#imSyncText").text(JSON.stringify(data, undefined, 2)); - }).fail(function(err) { - $("#imSyncText").text(JSON.stringify($.parseJSON(err.responseText))); - }); -}); diff --git a/jsfiddles/room_memberships/demo.css b/jsfiddles/room_memberships/demo.css deleted file mode 100644 index 48a55f372d..0000000000 --- a/jsfiddles/room_memberships/demo.css +++ /dev/null @@ -1,17 +0,0 @@ -.loggedin { - visibility: hidden; -} - -p { - font-family: monospace; -} - -table -{ - border-spacing:5px; -} - -th,td -{ - padding:5px; -} diff --git a/jsfiddles/room_memberships/demo.html b/jsfiddles/room_memberships/demo.html deleted file mode 100644 index e6f39df5aa..0000000000 --- a/jsfiddles/room_memberships/demo.html +++ /dev/null @@ -1,37 +0,0 @@ -
-

This room membership demo requires a home server to be running on http://localhost:8008

-
-
- - - -
-
-
- -
-
- - - - -
-
- - -
- - - - - - - - -
Room IDMy stateRoom Alias
-
- diff --git a/jsfiddles/room_memberships/demo.js b/jsfiddles/room_memberships/demo.js deleted file mode 100644 index 8a7b1aa88e..0000000000 --- a/jsfiddles/room_memberships/demo.js +++ /dev/null @@ -1,141 +0,0 @@ -var accountInfo = {}; - -var showLoggedIn = function(data) { - accountInfo = data; - getCurrentRoomList(); - $(".loggedin").css({visibility: "visible"}); - $("#membership").change(function() { - if ($("#membership").val() === "invite") { - $("#targetUser").css({visibility: "visible"}); - } - else { - $("#targetUser").css({visibility: "hidden"}); - } -}); -}; - -$('.login').live('click', function() { - var user = $("#userLogin").val(); - var password = $("#passwordLogin").val(); - $.ajax({ - url: "http://localhost:8008/_matrix/client/api/v1/login", - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), - dataType: "json", - success: function(data) { - $("#rooms").find("tr:gt(0)").remove(); - showLoggedIn(data); - }, - error: function(err) { - var errMsg = "To try this, you need a home server running!"; - var errJson = $.parseJSON(err.responseText); - if (errJson) { - errMsg = JSON.stringify(errJson); - } - alert(errMsg); - } - }); -}); - -var getCurrentRoomList = function() { - $("#roomId").val(""); - // wipe the table and reload it. Using the event stream would be the best - // solution but that is out of scope of this fiddle. - $("#rooms").find("tr:gt(0)").remove(); - - var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; - $.getJSON(url, function(data) { - var rooms = data.rooms; - for (var i=0; i"+data.room_id+"" + - ""+data.membership+"" + - ""+data.room_alias+"" + - ""; - $("#rooms").append(row); -}; - -$('.changeMembership').live('click', function() { - var roomId = $("#roomId").val(); - var member = $("#targetUser").val(); - var membership = $("#membership").val(); - - if (roomId.length === 0) { - return; - } - - var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token"; - url = url.replace("$token", accountInfo.access_token); - url = url.replace("$roomid", encodeURIComponent(roomId)); - url = url.replace("$membership", membership); - - var data = {}; - - if (membership === "invite") { - data = { - user_id: member - }; - } - - $.ajax({ - url: url, - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify(data), - dataType: "json", - success: function(data) { - getCurrentRoomList(); - }, - error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); - } - }); -}); - -$('.joinAlias').live('click', function() { - var roomAlias = $("#roomAlias").val(); - var url = "http://localhost:8008/_matrix/client/api/v1/join/$roomalias?access_token=$token"; - url = url.replace("$token", accountInfo.access_token); - url = url.replace("$roomalias", encodeURIComponent(roomAlias)); - $.ajax({ - url: url, - type: "POST", - contentType: "application/json; charset=utf-8", - data: JSON.stringify({}), - dataType: "json", - success: function(data) { - getCurrentRoomList(); - }, - error: function(err) { - alert(JSON.stringify($.parseJSON(err.responseText))); - } - }); -}); -- cgit 1.5.1 From d3d0713de587c97c3c23200a15215eb0807ff87e Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 15 Jan 2015 17:06:12 +0000 Subject: Move experiments, graph and cmdclient into contrib --- cmdclient/console.py | 747 ---------------------------------- cmdclient/http.py | 217 ---------- contrib/cmdclient/console.py | 747 ++++++++++++++++++++++++++++++++++ contrib/cmdclient/http.py | 217 ++++++++++ contrib/experiments/cursesio.py | 168 ++++++++ contrib/experiments/test_messaging.py | 394 ++++++++++++++++++ contrib/graph/graph.py | 151 +++++++ contrib/graph/graph2.py | 156 +++++++ experiments/cursesio.py | 168 -------- experiments/test_messaging.py | 394 ------------------ graph/graph.py | 151 ------- graph/graph2.py | 156 ------- 12 files changed, 1833 insertions(+), 1833 deletions(-) delete mode 100755 cmdclient/console.py delete mode 100644 cmdclient/http.py create mode 100755 contrib/cmdclient/console.py create mode 100644 contrib/cmdclient/http.py create mode 100644 contrib/experiments/cursesio.py create mode 100644 contrib/experiments/test_messaging.py create mode 100644 contrib/graph/graph.py create mode 100644 contrib/graph/graph2.py delete mode 100644 experiments/cursesio.py delete mode 100644 experiments/test_messaging.py delete mode 100644 graph/graph.py delete mode 100644 graph/graph2.py diff --git a/cmdclient/console.py b/cmdclient/console.py deleted file mode 100755 index d9c6ec6a70..0000000000 --- a/cmdclient/console.py +++ /dev/null @@ -1,747 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" Starts a synapse client console. """ - -from twisted.internet import reactor, defer, threads -from http import TwistedHttpClient - -import argparse -import cmd -import getpass -import json -import shlex -import sys -import time -import urllib -import urlparse - -import nacl.signing -import nacl.encoding - -from syutil.crypto.jsonsign import verify_signed_json, SignatureVerifyException - -CONFIG_JSON = "cmdclient_config.json" - -TRUSTED_ID_SERVERS = [ - 'localhost:8001' -] - -class SynapseCmd(cmd.Cmd): - - """Basic synapse command-line processor. - - This processes commands from the user and calls the relevant HTTP methods. - """ - - def __init__(self, http_client, server_url, identity_server_url, username, token): - cmd.Cmd.__init__(self) - self.http_client = http_client - self.http_client.verbose = True - self.config = { - "url": server_url, - "identityServerUrl": identity_server_url, - "user": username, - "token": token, - "verbose": "on", - "complete_usernames": "on", - "send_delivery_receipts": "on" - } - self.path_prefix = "/_matrix/client/api/v1" - self.event_stream_token = "END" - self.prompt = ">>> " - - def do_EOF(self, line): # allows CTRL+D quitting - return True - - def emptyline(self): - pass # else it repeats the previous command - - def _usr(self): - return self.config["user"] - - def _tok(self): - return self.config["token"] - - def _url(self): - return self.config["url"] + self.path_prefix - - def _identityServerUrl(self): - return self.config["identityServerUrl"] - - def _is_on(self, config_name): - if config_name in self.config: - return self.config[config_name] == "on" - return False - - def _domain(self): - if "user" not in self.config or not self.config["user"]: - return None - return self.config["user"].split(":")[1] - - def do_config(self, line): - """ Show the config for this client: "config" - Edit a key value mapping: "config key value" e.g. "config token 1234" - Config variables: - user: The username to auth with. - token: The access token to auth with. - url: The url of the server. - verbose: [on|off] The verbosity of requests/responses. - complete_usernames: [on|off] Auto complete partial usernames by - assuming they are on the same homeserver as you. - E.g. name >> @name:yourhost - send_delivery_receipts: [on|off] Automatically send receipts to - messages when performing a 'stream' command. - Additional key/values can be added and can be substituted into requests - by using $. E.g. 'config roomid room1' then 'raw get /rooms/$roomid'. - """ - if len(line) == 0: - print json.dumps(self.config, indent=4) - return - - try: - args = self._parse(line, ["key", "val"], force_keys=True) - - # make sure restricted config values are checked - config_rules = [ # key, valid_values - ("verbose", ["on", "off"]), - ("complete_usernames", ["on", "off"]), - ("send_delivery_receipts", ["on", "off"]) - ] - for key, valid_vals in config_rules: - if key == args["key"] and args["val"] not in valid_vals: - print "%s value must be one of %s" % (args["key"], - valid_vals) - return - - # toggle the http client verbosity - if args["key"] == "verbose": - self.http_client.verbose = "on" == args["val"] - - # assign the new config - self.config[args["key"]] = args["val"] - print json.dumps(self.config, indent=4) - - save_config(self.config) - except Exception as e: - print e - - def do_register(self, line): - """Registers for a new account: "register " - : The desired user ID - : Do not automatically clobber config values. - """ - args = self._parse(line, ["userid", "noupdate"]) - - password = None - pwd = None - pwd2 = "_" - while pwd != pwd2: - pwd = getpass.getpass("Type a password for this user: ") - pwd2 = getpass.getpass("Retype the password: ") - if pwd != pwd2 or len(pwd) == 0: - print "Password mismatch." - pwd = None - else: - password = pwd - - body = { - "type": "m.login.password" - } - if "userid" in args: - body["user"] = args["userid"] - if password: - body["password"] = password - - reactor.callFromThread(self._do_register, body, - "noupdate" not in args) - - @defer.inlineCallbacks - def _do_register(self, data, update_config): - # check the registration flows - url = self._url() + "/register" - json_res = yield self.http_client.do_request("GET", url) - print json.dumps(json_res, indent=4) - - passwordFlow = None - for flow in json_res["flows"]: - if flow["type"] == "m.login.recaptcha" or ("stages" in flow and "m.login.recaptcha" in flow["stages"]): - print "Unable to register: Home server requires captcha." - return - if flow["type"] == "m.login.password" and "stages" not in flow: - passwordFlow = flow - break - - if not passwordFlow: - return - - json_res = yield self.http_client.do_request("POST", url, data=data) - print json.dumps(json_res, indent=4) - if update_config and "user_id" in json_res: - self.config["user"] = json_res["user_id"] - self.config["token"] = json_res["access_token"] - save_config(self.config) - - def do_login(self, line): - """Login as a specific user: "login @bob:localhost" - You MAY be prompted for a password, or instructed to visit a URL. - """ - try: - args = self._parse(line, ["user_id"], force_keys=True) - can_login = threads.blockingCallFromThread( - reactor, - self._check_can_login) - if can_login: - p = getpass.getpass("Enter your password: ") - user = args["user_id"] - if self._is_on("complete_usernames") and not user.startswith("@"): - domain = self._domain() - if domain: - user = "@" + user + ":" + domain - - reactor.callFromThread(self._do_login, user, p) - #print " got %s " % p - except Exception as e: - print e - - @defer.inlineCallbacks - def _do_login(self, user, password): - path = "/login" - data = { - "user": user, - "password": password, - "type": "m.login.password" - } - url = self._url() + path - json_res = yield self.http_client.do_request("POST", url, data=data) - print json_res - - if "access_token" in json_res: - self.config["user"] = user - self.config["token"] = json_res["access_token"] - save_config(self.config) - print "Login successful." - - @defer.inlineCallbacks - def _check_can_login(self): - path = "/login" - # ALWAYS check that the home server can handle the login request before - # submitting! - url = self._url() + path - json_res = yield self.http_client.do_request("GET", url) - print json_res - - if "flows" not in json_res: - print "Failed to find any login flows." - defer.returnValue(False) - - flow = json_res["flows"][0] # assume first is the one we want. - if ("type" not in flow or "m.login.password" != flow["type"] or - "stages" in flow): - fallback_url = self._url() + "/login/fallback" - print ("Unable to login via the command line client. Please visit " - "%s to login." % fallback_url) - defer.returnValue(False) - defer.returnValue(True) - - def do_emailrequest(self, line): - """Requests the association of a third party identifier -
The email address) - A string of characters generated when requesting an email that you'll supply in subsequent calls to identify yourself - The number of times the user has requested an email. Leave this the same between requests to retry the request at the transport level. Increment it to request that the email be sent again. - """ - args = self._parse(line, ['address', 'clientSecret', 'sendAttempt']) - - postArgs = {'email': args['address'], 'clientSecret': args['clientSecret'], 'sendAttempt': args['sendAttempt']} - - reactor.callFromThread(self._do_emailrequest, postArgs) - - @defer.inlineCallbacks - def _do_emailrequest(self, args): - url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/requestToken" - - json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, - headers={'Content-Type': ['application/x-www-form-urlencoded']}) - print json_res - if 'sid' in json_res: - print "Token sent. Your session ID is %s" % (json_res['sid']) - - def do_emailvalidate(self, line): - """Validate and associate a third party ID - The session ID (sid) given to you in the response to requestToken - The token sent to your third party identifier address - The same clientSecret you supplied in requestToken - """ - args = self._parse(line, ['sid', 'token', 'clientSecret']) - - postArgs = { 'sid' : args['sid'], 'token' : args['token'], 'clientSecret': args['clientSecret'] } - - reactor.callFromThread(self._do_emailvalidate, postArgs) - - @defer.inlineCallbacks - def _do_emailvalidate(self, args): - url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/submitToken" - - json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, - headers={'Content-Type': ['application/x-www-form-urlencoded']}) - print json_res - - def do_3pidbind(self, line): - """Validate and associate a third party ID - The session ID (sid) given to you in the response to requestToken - The same clientSecret you supplied in requestToken - """ - args = self._parse(line, ['sid', 'clientSecret']) - - postArgs = { 'sid' : args['sid'], 'clientSecret': args['clientSecret'] } - postArgs['mxid'] = self.config["user"] - - reactor.callFromThread(self._do_3pidbind, postArgs) - - @defer.inlineCallbacks - def _do_3pidbind(self, args): - url = self._identityServerUrl()+"/_matrix/identity/api/v1/3pid/bind" - - json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, - headers={'Content-Type': ['application/x-www-form-urlencoded']}) - print json_res - - def do_join(self, line): - """Joins a room: "join " """ - try: - args = self._parse(line, ["roomid"], force_keys=True) - self._do_membership_change(args["roomid"], "join", self._usr()) - except Exception as e: - print e - - def do_joinalias(self, line): - try: - args = self._parse(line, ["roomname"], force_keys=True) - path = "/join/%s" % urllib.quote(args["roomname"]) - reactor.callFromThread(self._run_and_pprint, "POST", path, {}) - except Exception as e: - print e - - def do_topic(self, line): - """"topic [set|get] []" - Set the topic for a room: topic set - Get the topic for a room: topic get - """ - try: - args = self._parse(line, ["action", "roomid", "topic"]) - if "action" not in args or "roomid" not in args: - print "Must specify set|get and a room ID." - return - if args["action"].lower() not in ["set", "get"]: - print "Must specify set|get, not %s" % args["action"] - return - - path = "/rooms/%s/topic" % urllib.quote(args["roomid"]) - - if args["action"].lower() == "set": - if "topic" not in args: - print "Must specify a new topic." - return - body = { - "topic": args["topic"] - } - reactor.callFromThread(self._run_and_pprint, "PUT", path, body) - elif args["action"].lower() == "get": - reactor.callFromThread(self._run_and_pprint, "GET", path) - except Exception as e: - print e - - def do_invite(self, line): - """Invite a user to a room: "invite " """ - try: - args = self._parse(line, ["userid", "roomid"], force_keys=True) - - user_id = args["userid"] - - reactor.callFromThread(self._do_invite, args["roomid"], user_id) - except Exception as e: - print e - - @defer.inlineCallbacks - def _do_invite(self, roomid, userstring): - if (not userstring.startswith('@') and - self._is_on("complete_usernames")): - url = self._identityServerUrl()+"/_matrix/identity/api/v1/lookup" - - json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring}) - - mxid = None - - if 'mxid' in json_res and 'signatures' in json_res: - url = self._identityServerUrl()+"/_matrix/identity/api/v1/pubkey/ed25519" - - pubKey = None - pubKeyObj = yield self.http_client.do_request("GET", url) - if 'public_key' in pubKeyObj: - pubKey = nacl.signing.VerifyKey(pubKeyObj['public_key'], encoder=nacl.encoding.HexEncoder) - else: - print "No public key found in pubkey response!" - - sigValid = False - - if pubKey: - for signame in json_res['signatures']: - if signame not in TRUSTED_ID_SERVERS: - print "Ignoring signature from untrusted server %s" % (signame) - else: - try: - verify_signed_json(json_res, signame, pubKey) - sigValid = True - print "Mapping %s -> %s correctly signed by %s" % (userstring, json_res['mxid'], signame) - break - except SignatureVerifyException as e: - print "Invalid signature from %s" % (signame) - print e - - if sigValid: - print "Resolved 3pid %s to %s" % (userstring, json_res['mxid']) - mxid = json_res['mxid'] - else: - print "Got association for %s but couldn't verify signature" % (userstring) - - if not mxid: - mxid = "@" + userstring + ":" + self._domain() - - self._do_membership_change(roomid, "invite", mxid) - - def do_leave(self, line): - """Leaves a room: "leave " """ - try: - args = self._parse(line, ["roomid"], force_keys=True) - self._do_membership_change(args["roomid"], "leave", self._usr()) - except Exception as e: - print e - - def do_send(self, line): - """Sends a message. "send " """ - args = self._parse(line, ["roomid", "body"]) - txn_id = "txn%s" % int(time.time()) - path = "/rooms/%s/send/m.room.message/%s" % (urllib.quote(args["roomid"]), - txn_id) - body_json = { - "msgtype": "m.text", - "body": args["body"] - } - reactor.callFromThread(self._run_and_pprint, "PUT", path, body_json) - - def do_list(self, line): - """List data about a room. - "list members [query]" - List all the members in this room. - "list messages [query]" - List all the messages in this room. - - Where [query] will be directly applied as query parameters, allowing - you to use the pagination API. E.g. the last 3 messages in this room: - "list messages from=END&to=START&limit=3" - """ - args = self._parse(line, ["type", "roomid", "qp"]) - if not "type" in args or not "roomid" in args: - print "Must specify type and room ID." - return - if args["type"] not in ["members", "messages"]: - print "Unrecognised type: %s" % args["type"] - return - room_id = args["roomid"] - path = "/rooms/%s/%s" % (urllib.quote(room_id), args["type"]) - - qp = {"access_token": self._tok()} - if "qp" in args: - for key_value_str in args["qp"].split("&"): - try: - key_value = key_value_str.split("=") - qp[key_value[0]] = key_value[1] - except: - print "Bad query param: %s" % key_value - return - - reactor.callFromThread(self._run_and_pprint, "GET", path, - query_params=qp) - - def do_create(self, line): - """Creates a room. - "create [public|private] " - Create a room with the - specified visibility. - "create " - Create a room with default visibility. - "create [public|private]" - Create a room with specified visibility. - "create" - Create a room with default visibility. - """ - args = self._parse(line, ["vis", "roomname"]) - # fixup args depending on which were set - body = {} - if "vis" in args and args["vis"] in ["public", "private"]: - body["visibility"] = args["vis"] - - if "roomname" in args: - room_name = args["roomname"] - body["room_alias_name"] = room_name - elif "vis" in args and args["vis"] not in ["public", "private"]: - room_name = args["vis"] - body["room_alias_name"] = room_name - - reactor.callFromThread(self._run_and_pprint, "POST", "/createRoom", body) - - def do_raw(self, line): - """Directly send a JSON object: "raw " - : Required. One of "PUT", "GET", "POST", "xPUT", "xGET", - "xPOST". Methods with 'x' prefixed will not automatically append the - access token. - : Required. E.g. "/events" - : Optional. E.g. "{ "msgtype":"custom.text", "body":"abc123"}" - """ - args = self._parse(line, ["method", "path", "data"]) - # sanity check - if "method" not in args or "path" not in args: - print "Must specify path and method." - return - - args["method"] = args["method"].upper() - valid_methods = ["PUT", "GET", "POST", "DELETE", - "XPUT", "XGET", "XPOST", "XDELETE"] - if args["method"] not in valid_methods: - print "Unsupported method: %s" % args["method"] - return - - if "data" not in args: - args["data"] = None - else: - try: - args["data"] = json.loads(args["data"]) - except Exception as e: - print "Data is not valid JSON. %s" % e - return - - qp = {"access_token": self._tok()} - if args["method"].startswith("X"): - qp = {} # remove access token - args["method"] = args["method"][1:] # snip the X - else: - # append any query params the user has set - try: - parsed_url = urlparse.urlparse(args["path"]) - qp.update(urlparse.parse_qs(parsed_url.query)) - args["path"] = parsed_url.path - except: - pass - - reactor.callFromThread(self._run_and_pprint, args["method"], - args["path"], - args["data"], - query_params=qp) - - def do_stream(self, line): - """Stream data from the server: "stream " """ - args = self._parse(line, ["timeout"]) - timeout = 5000 - if "timeout" in args: - try: - timeout = int(args["timeout"]) - except ValueError: - print "Timeout must be in milliseconds." - return - reactor.callFromThread(self._do_event_stream, timeout) - - @defer.inlineCallbacks - def _do_event_stream(self, timeout): - res = yield self.http_client.get_json( - self._url() + "/events", - { - "access_token": self._tok(), - "timeout": str(timeout), - "from": self.event_stream_token - }) - print json.dumps(res, indent=4) - - if "chunk" in res: - for event in res["chunk"]: - if (event["type"] == "m.room.message" and - self._is_on("send_delivery_receipts") and - event["user_id"] != self._usr()): # not sent by us - self._send_receipt(event, "d") - - # update the position in the stram - if "end" in res: - self.event_stream_token = res["end"] - - def _send_receipt(self, event, feedback_type): - path = ("/rooms/%s/messages/%s/%s/feedback/%s/%s" % - (urllib.quote(event["room_id"]), event["user_id"], event["msg_id"], - self._usr(), feedback_type)) - data = {} - reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data, - alt_text="Sent receipt for %s" % event["msg_id"]) - - def _do_membership_change(self, roomid, membership, userid): - path = "/rooms/%s/state/m.room.member/%s" % (urllib.quote(roomid), urllib.quote(userid)) - data = { - "membership": membership - } - reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) - - def do_displayname(self, line): - """Get or set my displayname: "displayname [new_name]" """ - args = self._parse(line, ["name"]) - path = "/profile/%s/displayname" % (self.config["user"]) - - if "name" in args: - data = {"displayname": args["name"]} - reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) - else: - reactor.callFromThread(self._run_and_pprint, "GET", path) - - def _do_presence_state(self, state, line): - args = self._parse(line, ["msgstring"]) - path = "/presence/%s/status" % (self.config["user"]) - data = {"state": state} - if "msgstring" in args: - data["status_msg"] = args["msgstring"] - - reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) - - def do_offline(self, line): - """Set my presence state to OFFLINE""" - self._do_presence_state(0, line) - - def do_away(self, line): - """Set my presence state to AWAY""" - self._do_presence_state(1, line) - - def do_online(self, line): - """Set my presence state to ONLINE""" - self._do_presence_state(2, line) - - def _parse(self, line, keys, force_keys=False): - """ Parses the given line. - - Args: - line : The line to parse - keys : A list of keys to map onto the args - force_keys : True to enforce that the line has a value for every key - Returns: - A dict of key:arg - """ - line_args = shlex.split(line) - if force_keys and len(line_args) != len(keys): - raise IndexError("Must specify all args: %s" % keys) - - # do $ substitutions - for i, arg in enumerate(line_args): - for config_key in self.config: - if ("$" + config_key) in arg: - arg = arg.replace("$" + config_key, - self.config[config_key]) - line_args[i] = arg - - return dict(zip(keys, line_args)) - - @defer.inlineCallbacks - def _run_and_pprint(self, method, path, data=None, - query_params={"access_token": None}, alt_text=None): - """ Runs an HTTP request and pretty prints the output. - - Args: - method: HTTP method - path: Relative path - data: Raw JSON data if any - query_params: dict of query parameters to add to the url - """ - url = self._url() + path - if "access_token" in query_params: - query_params["access_token"] = self._tok() - - json_res = yield self.http_client.do_request(method, url, - data=data, - qparams=query_params) - if alt_text: - print alt_text - else: - print json.dumps(json_res, indent=4) - - -def save_config(config): - with open(CONFIG_JSON, 'w') as out: - json.dump(config, out) - - -def main(server_url, identity_server_url, username, token, config_path): - print "Synapse command line client" - print "===========================" - print "Server: %s" % server_url - print "Type 'help' to get started." - print "Close this console with CTRL+C then CTRL+D." - if not username or not token: - print "- 'register ' - Register an account" - print "- 'stream' - Connect to the event stream" - print "- 'create ' - Create a room" - print "- 'send ' - Send a message" - http_client = TwistedHttpClient() - - # the command line client - syn_cmd = SynapseCmd(http_client, server_url, identity_server_url, username, token) - - # load synapse.json config from a previous session - global CONFIG_JSON - CONFIG_JSON = config_path # bit cheeky, but just overwrite the global - try: - with open(config_path, 'r') as config: - syn_cmd.config = json.load(config) - try: - http_client.verbose = "on" == syn_cmd.config["verbose"] - except: - pass - print "Loaded config from %s" % config_path - except: - pass - - # Twisted-specific: Runs the command processor in Twisted's event loop - # to maintain a single thread for both commands and event processing. - # If using another HTTP client, just call syn_cmd.cmdloop() - reactor.callInThread(syn_cmd.cmdloop) - reactor.run() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser("Starts a synapse client.") - parser.add_argument( - "-s", "--server", dest="server", default="http://localhost:8008", - help="The URL of the home server to talk to.") - parser.add_argument( - "-i", "--identity-server", dest="identityserver", default="http://localhost:8090", - help="The URL of the identity server to talk to.") - parser.add_argument( - "-u", "--username", dest="username", - help="Your username on the server.") - parser.add_argument( - "-t", "--token", dest="token", - help="Your access token.") - parser.add_argument( - "-c", "--config", dest="config", default=CONFIG_JSON, - help="The location of the config.json file to read from.") - args = parser.parse_args() - - if not args.server: - print "You must supply a server URL to communicate with." - parser.print_help() - sys.exit(1) - - server = args.server - if not server.startswith("http://"): - server = "http://" + args.server - - main(server, args.identityserver, args.username, args.token, args.config) diff --git a/cmdclient/http.py b/cmdclient/http.py deleted file mode 100644 index 869f782ec1..0000000000 --- a/cmdclient/http.py +++ /dev/null @@ -1,217 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.web.client import Agent, readBody -from twisted.web.http_headers import Headers -from twisted.internet import defer, reactor - -from pprint import pformat - -import json -import urllib - - -class HttpClient(object): - """ Interface for talking json over http - """ - - def put_json(self, url, data): - """ Sends the specifed json data using PUT - - Args: - url (str): The URL to PUT data to. - data (dict): A dict containing the data that will be used as - the request body. This will be encoded as JSON. - - Returns: - Deferred: Succeeds when we get *any* HTTP response. - - The result of the deferred is a tuple of `(code, response)`, - where `response` is a dict representing the decoded JSON body. - """ - pass - - def get_json(self, url, args=None): - """ Get's some json from the given host homeserver and path - - Args: - url (str): The URL to GET data from. - args (dict): A dictionary used to create query strings, defaults to - None. - **Note**: The value of each key is assumed to be an iterable - and *not* a string. - - Returns: - Deferred: Succeeds when we get *any* HTTP response. - - The result of the deferred is a tuple of `(code, response)`, - where `response` is a dict representing the decoded JSON body. - """ - pass - - -class TwistedHttpClient(HttpClient): - """ Wrapper around the twisted HTTP client api. - - Attributes: - agent (twisted.web.client.Agent): The twisted Agent used to send the - requests. - """ - - def __init__(self): - self.agent = Agent(reactor) - - @defer.inlineCallbacks - def put_json(self, url, data): - response = yield self._create_put_request( - url, - data, - headers_dict={"Content-Type": ["application/json"]} - ) - body = yield readBody(response) - defer.returnValue((response.code, body)) - - @defer.inlineCallbacks - def get_json(self, url, args=None): - if args: - # generates a list of strings of form "k=v". - qs = urllib.urlencode(args, True) - url = "%s?%s" % (url, qs) - response = yield self._create_get_request(url) - body = yield readBody(response) - defer.returnValue(json.loads(body)) - - def _create_put_request(self, url, json_data, headers_dict={}): - """ Wrapper of _create_request to issue a PUT request - """ - - if "Content-Type" not in headers_dict: - raise defer.error( - RuntimeError("Must include Content-Type header for PUTs")) - - return self._create_request( - "PUT", - url, - producer=_JsonProducer(json_data), - headers_dict=headers_dict - ) - - def _create_get_request(self, url, headers_dict={}): - """ Wrapper of _create_request to issue a GET request - """ - return self._create_request( - "GET", - url, - headers_dict=headers_dict - ) - - @defer.inlineCallbacks - def do_request(self, method, url, data=None, qparams=None, jsonreq=True, headers={}): - if qparams: - url = "%s?%s" % (url, urllib.urlencode(qparams, True)) - - if jsonreq: - prod = _JsonProducer(data) - headers['Content-Type'] = ["application/json"]; - else: - prod = _RawProducer(data) - - if method in ["POST", "PUT"]: - response = yield self._create_request(method, url, - producer=prod, - headers_dict=headers) - else: - response = yield self._create_request(method, url) - - body = yield readBody(response) - defer.returnValue(json.loads(body)) - - @defer.inlineCallbacks - def _create_request(self, method, url, producer=None, headers_dict={}): - """ Creates and sends a request to the given url - """ - headers_dict["User-Agent"] = ["Synapse Cmd Client"] - - retries_left = 5 - print "%s to %s with headers %s" % (method, url, headers_dict) - if self.verbose and producer: - if "password" in producer.data: - temp = producer.data["password"] - producer.data["password"] = "[REDACTED]" - print json.dumps(producer.data, indent=4) - producer.data["password"] = temp - else: - print json.dumps(producer.data, indent=4) - - while True: - try: - response = yield self.agent.request( - method, - url.encode("UTF8"), - Headers(headers_dict), - producer - ) - break - except Exception as e: - print "uh oh: %s" % e - if retries_left: - yield self.sleep(2 ** (5 - retries_left)) - retries_left -= 1 - else: - raise e - - if self.verbose: - print "Status %s %s" % (response.code, response.phrase) - print pformat(list(response.headers.getAllRawHeaders())) - defer.returnValue(response) - - def sleep(self, seconds): - d = defer.Deferred() - reactor.callLater(seconds, d.callback, seconds) - return d - -class _RawProducer(object): - def __init__(self, data): - self.data = data - self.body = data - self.length = len(self.body) - - def startProducing(self, consumer): - consumer.write(self.body) - return defer.succeed(None) - - def pauseProducing(self): - pass - - def stopProducing(self): - pass - -class _JsonProducer(object): - """ Used by the twisted http client to create the HTTP body from json - """ - def __init__(self, jsn): - self.data = jsn - self.body = json.dumps(jsn).encode("utf8") - self.length = len(self.body) - - def startProducing(self, consumer): - consumer.write(self.body) - return defer.succeed(None) - - def pauseProducing(self): - pass - - def stopProducing(self): - pass \ No newline at end of file diff --git a/contrib/cmdclient/console.py b/contrib/cmdclient/console.py new file mode 100755 index 0000000000..d9c6ec6a70 --- /dev/null +++ b/contrib/cmdclient/console.py @@ -0,0 +1,747 @@ +#!/usr/bin/env python + +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Starts a synapse client console. """ + +from twisted.internet import reactor, defer, threads +from http import TwistedHttpClient + +import argparse +import cmd +import getpass +import json +import shlex +import sys +import time +import urllib +import urlparse + +import nacl.signing +import nacl.encoding + +from syutil.crypto.jsonsign import verify_signed_json, SignatureVerifyException + +CONFIG_JSON = "cmdclient_config.json" + +TRUSTED_ID_SERVERS = [ + 'localhost:8001' +] + +class SynapseCmd(cmd.Cmd): + + """Basic synapse command-line processor. + + This processes commands from the user and calls the relevant HTTP methods. + """ + + def __init__(self, http_client, server_url, identity_server_url, username, token): + cmd.Cmd.__init__(self) + self.http_client = http_client + self.http_client.verbose = True + self.config = { + "url": server_url, + "identityServerUrl": identity_server_url, + "user": username, + "token": token, + "verbose": "on", + "complete_usernames": "on", + "send_delivery_receipts": "on" + } + self.path_prefix = "/_matrix/client/api/v1" + self.event_stream_token = "END" + self.prompt = ">>> " + + def do_EOF(self, line): # allows CTRL+D quitting + return True + + def emptyline(self): + pass # else it repeats the previous command + + def _usr(self): + return self.config["user"] + + def _tok(self): + return self.config["token"] + + def _url(self): + return self.config["url"] + self.path_prefix + + def _identityServerUrl(self): + return self.config["identityServerUrl"] + + def _is_on(self, config_name): + if config_name in self.config: + return self.config[config_name] == "on" + return False + + def _domain(self): + if "user" not in self.config or not self.config["user"]: + return None + return self.config["user"].split(":")[1] + + def do_config(self, line): + """ Show the config for this client: "config" + Edit a key value mapping: "config key value" e.g. "config token 1234" + Config variables: + user: The username to auth with. + token: The access token to auth with. + url: The url of the server. + verbose: [on|off] The verbosity of requests/responses. + complete_usernames: [on|off] Auto complete partial usernames by + assuming they are on the same homeserver as you. + E.g. name >> @name:yourhost + send_delivery_receipts: [on|off] Automatically send receipts to + messages when performing a 'stream' command. + Additional key/values can be added and can be substituted into requests + by using $. E.g. 'config roomid room1' then 'raw get /rooms/$roomid'. + """ + if len(line) == 0: + print json.dumps(self.config, indent=4) + return + + try: + args = self._parse(line, ["key", "val"], force_keys=True) + + # make sure restricted config values are checked + config_rules = [ # key, valid_values + ("verbose", ["on", "off"]), + ("complete_usernames", ["on", "off"]), + ("send_delivery_receipts", ["on", "off"]) + ] + for key, valid_vals in config_rules: + if key == args["key"] and args["val"] not in valid_vals: + print "%s value must be one of %s" % (args["key"], + valid_vals) + return + + # toggle the http client verbosity + if args["key"] == "verbose": + self.http_client.verbose = "on" == args["val"] + + # assign the new config + self.config[args["key"]] = args["val"] + print json.dumps(self.config, indent=4) + + save_config(self.config) + except Exception as e: + print e + + def do_register(self, line): + """Registers for a new account: "register " + : The desired user ID + : Do not automatically clobber config values. + """ + args = self._parse(line, ["userid", "noupdate"]) + + password = None + pwd = None + pwd2 = "_" + while pwd != pwd2: + pwd = getpass.getpass("Type a password for this user: ") + pwd2 = getpass.getpass("Retype the password: ") + if pwd != pwd2 or len(pwd) == 0: + print "Password mismatch." + pwd = None + else: + password = pwd + + body = { + "type": "m.login.password" + } + if "userid" in args: + body["user"] = args["userid"] + if password: + body["password"] = password + + reactor.callFromThread(self._do_register, body, + "noupdate" not in args) + + @defer.inlineCallbacks + def _do_register(self, data, update_config): + # check the registration flows + url = self._url() + "/register" + json_res = yield self.http_client.do_request("GET", url) + print json.dumps(json_res, indent=4) + + passwordFlow = None + for flow in json_res["flows"]: + if flow["type"] == "m.login.recaptcha" or ("stages" in flow and "m.login.recaptcha" in flow["stages"]): + print "Unable to register: Home server requires captcha." + return + if flow["type"] == "m.login.password" and "stages" not in flow: + passwordFlow = flow + break + + if not passwordFlow: + return + + json_res = yield self.http_client.do_request("POST", url, data=data) + print json.dumps(json_res, indent=4) + if update_config and "user_id" in json_res: + self.config["user"] = json_res["user_id"] + self.config["token"] = json_res["access_token"] + save_config(self.config) + + def do_login(self, line): + """Login as a specific user: "login @bob:localhost" + You MAY be prompted for a password, or instructed to visit a URL. + """ + try: + args = self._parse(line, ["user_id"], force_keys=True) + can_login = threads.blockingCallFromThread( + reactor, + self._check_can_login) + if can_login: + p = getpass.getpass("Enter your password: ") + user = args["user_id"] + if self._is_on("complete_usernames") and not user.startswith("@"): + domain = self._domain() + if domain: + user = "@" + user + ":" + domain + + reactor.callFromThread(self._do_login, user, p) + #print " got %s " % p + except Exception as e: + print e + + @defer.inlineCallbacks + def _do_login(self, user, password): + path = "/login" + data = { + "user": user, + "password": password, + "type": "m.login.password" + } + url = self._url() + path + json_res = yield self.http_client.do_request("POST", url, data=data) + print json_res + + if "access_token" in json_res: + self.config["user"] = user + self.config["token"] = json_res["access_token"] + save_config(self.config) + print "Login successful." + + @defer.inlineCallbacks + def _check_can_login(self): + path = "/login" + # ALWAYS check that the home server can handle the login request before + # submitting! + url = self._url() + path + json_res = yield self.http_client.do_request("GET", url) + print json_res + + if "flows" not in json_res: + print "Failed to find any login flows." + defer.returnValue(False) + + flow = json_res["flows"][0] # assume first is the one we want. + if ("type" not in flow or "m.login.password" != flow["type"] or + "stages" in flow): + fallback_url = self._url() + "/login/fallback" + print ("Unable to login via the command line client. Please visit " + "%s to login." % fallback_url) + defer.returnValue(False) + defer.returnValue(True) + + def do_emailrequest(self, line): + """Requests the association of a third party identifier +
The email address) + A string of characters generated when requesting an email that you'll supply in subsequent calls to identify yourself + The number of times the user has requested an email. Leave this the same between requests to retry the request at the transport level. Increment it to request that the email be sent again. + """ + args = self._parse(line, ['address', 'clientSecret', 'sendAttempt']) + + postArgs = {'email': args['address'], 'clientSecret': args['clientSecret'], 'sendAttempt': args['sendAttempt']} + + reactor.callFromThread(self._do_emailrequest, postArgs) + + @defer.inlineCallbacks + def _do_emailrequest(self, args): + url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/requestToken" + + json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, + headers={'Content-Type': ['application/x-www-form-urlencoded']}) + print json_res + if 'sid' in json_res: + print "Token sent. Your session ID is %s" % (json_res['sid']) + + def do_emailvalidate(self, line): + """Validate and associate a third party ID + The session ID (sid) given to you in the response to requestToken + The token sent to your third party identifier address + The same clientSecret you supplied in requestToken + """ + args = self._parse(line, ['sid', 'token', 'clientSecret']) + + postArgs = { 'sid' : args['sid'], 'token' : args['token'], 'clientSecret': args['clientSecret'] } + + reactor.callFromThread(self._do_emailvalidate, postArgs) + + @defer.inlineCallbacks + def _do_emailvalidate(self, args): + url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/submitToken" + + json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, + headers={'Content-Type': ['application/x-www-form-urlencoded']}) + print json_res + + def do_3pidbind(self, line): + """Validate and associate a third party ID + The session ID (sid) given to you in the response to requestToken + The same clientSecret you supplied in requestToken + """ + args = self._parse(line, ['sid', 'clientSecret']) + + postArgs = { 'sid' : args['sid'], 'clientSecret': args['clientSecret'] } + postArgs['mxid'] = self.config["user"] + + reactor.callFromThread(self._do_3pidbind, postArgs) + + @defer.inlineCallbacks + def _do_3pidbind(self, args): + url = self._identityServerUrl()+"/_matrix/identity/api/v1/3pid/bind" + + json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False, + headers={'Content-Type': ['application/x-www-form-urlencoded']}) + print json_res + + def do_join(self, line): + """Joins a room: "join " """ + try: + args = self._parse(line, ["roomid"], force_keys=True) + self._do_membership_change(args["roomid"], "join", self._usr()) + except Exception as e: + print e + + def do_joinalias(self, line): + try: + args = self._parse(line, ["roomname"], force_keys=True) + path = "/join/%s" % urllib.quote(args["roomname"]) + reactor.callFromThread(self._run_and_pprint, "POST", path, {}) + except Exception as e: + print e + + def do_topic(self, line): + """"topic [set|get] []" + Set the topic for a room: topic set + Get the topic for a room: topic get + """ + try: + args = self._parse(line, ["action", "roomid", "topic"]) + if "action" not in args or "roomid" not in args: + print "Must specify set|get and a room ID." + return + if args["action"].lower() not in ["set", "get"]: + print "Must specify set|get, not %s" % args["action"] + return + + path = "/rooms/%s/topic" % urllib.quote(args["roomid"]) + + if args["action"].lower() == "set": + if "topic" not in args: + print "Must specify a new topic." + return + body = { + "topic": args["topic"] + } + reactor.callFromThread(self._run_and_pprint, "PUT", path, body) + elif args["action"].lower() == "get": + reactor.callFromThread(self._run_and_pprint, "GET", path) + except Exception as e: + print e + + def do_invite(self, line): + """Invite a user to a room: "invite " """ + try: + args = self._parse(line, ["userid", "roomid"], force_keys=True) + + user_id = args["userid"] + + reactor.callFromThread(self._do_invite, args["roomid"], user_id) + except Exception as e: + print e + + @defer.inlineCallbacks + def _do_invite(self, roomid, userstring): + if (not userstring.startswith('@') and + self._is_on("complete_usernames")): + url = self._identityServerUrl()+"/_matrix/identity/api/v1/lookup" + + json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring}) + + mxid = None + + if 'mxid' in json_res and 'signatures' in json_res: + url = self._identityServerUrl()+"/_matrix/identity/api/v1/pubkey/ed25519" + + pubKey = None + pubKeyObj = yield self.http_client.do_request("GET", url) + if 'public_key' in pubKeyObj: + pubKey = nacl.signing.VerifyKey(pubKeyObj['public_key'], encoder=nacl.encoding.HexEncoder) + else: + print "No public key found in pubkey response!" + + sigValid = False + + if pubKey: + for signame in json_res['signatures']: + if signame not in TRUSTED_ID_SERVERS: + print "Ignoring signature from untrusted server %s" % (signame) + else: + try: + verify_signed_json(json_res, signame, pubKey) + sigValid = True + print "Mapping %s -> %s correctly signed by %s" % (userstring, json_res['mxid'], signame) + break + except SignatureVerifyException as e: + print "Invalid signature from %s" % (signame) + print e + + if sigValid: + print "Resolved 3pid %s to %s" % (userstring, json_res['mxid']) + mxid = json_res['mxid'] + else: + print "Got association for %s but couldn't verify signature" % (userstring) + + if not mxid: + mxid = "@" + userstring + ":" + self._domain() + + self._do_membership_change(roomid, "invite", mxid) + + def do_leave(self, line): + """Leaves a room: "leave " """ + try: + args = self._parse(line, ["roomid"], force_keys=True) + self._do_membership_change(args["roomid"], "leave", self._usr()) + except Exception as e: + print e + + def do_send(self, line): + """Sends a message. "send " """ + args = self._parse(line, ["roomid", "body"]) + txn_id = "txn%s" % int(time.time()) + path = "/rooms/%s/send/m.room.message/%s" % (urllib.quote(args["roomid"]), + txn_id) + body_json = { + "msgtype": "m.text", + "body": args["body"] + } + reactor.callFromThread(self._run_and_pprint, "PUT", path, body_json) + + def do_list(self, line): + """List data about a room. + "list members [query]" - List all the members in this room. + "list messages [query]" - List all the messages in this room. + + Where [query] will be directly applied as query parameters, allowing + you to use the pagination API. E.g. the last 3 messages in this room: + "list messages from=END&to=START&limit=3" + """ + args = self._parse(line, ["type", "roomid", "qp"]) + if not "type" in args or not "roomid" in args: + print "Must specify type and room ID." + return + if args["type"] not in ["members", "messages"]: + print "Unrecognised type: %s" % args["type"] + return + room_id = args["roomid"] + path = "/rooms/%s/%s" % (urllib.quote(room_id), args["type"]) + + qp = {"access_token": self._tok()} + if "qp" in args: + for key_value_str in args["qp"].split("&"): + try: + key_value = key_value_str.split("=") + qp[key_value[0]] = key_value[1] + except: + print "Bad query param: %s" % key_value + return + + reactor.callFromThread(self._run_and_pprint, "GET", path, + query_params=qp) + + def do_create(self, line): + """Creates a room. + "create [public|private] " - Create a room with the + specified visibility. + "create " - Create a room with default visibility. + "create [public|private]" - Create a room with specified visibility. + "create" - Create a room with default visibility. + """ + args = self._parse(line, ["vis", "roomname"]) + # fixup args depending on which were set + body = {} + if "vis" in args and args["vis"] in ["public", "private"]: + body["visibility"] = args["vis"] + + if "roomname" in args: + room_name = args["roomname"] + body["room_alias_name"] = room_name + elif "vis" in args and args["vis"] not in ["public", "private"]: + room_name = args["vis"] + body["room_alias_name"] = room_name + + reactor.callFromThread(self._run_and_pprint, "POST", "/createRoom", body) + + def do_raw(self, line): + """Directly send a JSON object: "raw " + : Required. One of "PUT", "GET", "POST", "xPUT", "xGET", + "xPOST". Methods with 'x' prefixed will not automatically append the + access token. + : Required. E.g. "/events" + : Optional. E.g. "{ "msgtype":"custom.text", "body":"abc123"}" + """ + args = self._parse(line, ["method", "path", "data"]) + # sanity check + if "method" not in args or "path" not in args: + print "Must specify path and method." + return + + args["method"] = args["method"].upper() + valid_methods = ["PUT", "GET", "POST", "DELETE", + "XPUT", "XGET", "XPOST", "XDELETE"] + if args["method"] not in valid_methods: + print "Unsupported method: %s" % args["method"] + return + + if "data" not in args: + args["data"] = None + else: + try: + args["data"] = json.loads(args["data"]) + except Exception as e: + print "Data is not valid JSON. %s" % e + return + + qp = {"access_token": self._tok()} + if args["method"].startswith("X"): + qp = {} # remove access token + args["method"] = args["method"][1:] # snip the X + else: + # append any query params the user has set + try: + parsed_url = urlparse.urlparse(args["path"]) + qp.update(urlparse.parse_qs(parsed_url.query)) + args["path"] = parsed_url.path + except: + pass + + reactor.callFromThread(self._run_and_pprint, args["method"], + args["path"], + args["data"], + query_params=qp) + + def do_stream(self, line): + """Stream data from the server: "stream " """ + args = self._parse(line, ["timeout"]) + timeout = 5000 + if "timeout" in args: + try: + timeout = int(args["timeout"]) + except ValueError: + print "Timeout must be in milliseconds." + return + reactor.callFromThread(self._do_event_stream, timeout) + + @defer.inlineCallbacks + def _do_event_stream(self, timeout): + res = yield self.http_client.get_json( + self._url() + "/events", + { + "access_token": self._tok(), + "timeout": str(timeout), + "from": self.event_stream_token + }) + print json.dumps(res, indent=4) + + if "chunk" in res: + for event in res["chunk"]: + if (event["type"] == "m.room.message" and + self._is_on("send_delivery_receipts") and + event["user_id"] != self._usr()): # not sent by us + self._send_receipt(event, "d") + + # update the position in the stram + if "end" in res: + self.event_stream_token = res["end"] + + def _send_receipt(self, event, feedback_type): + path = ("/rooms/%s/messages/%s/%s/feedback/%s/%s" % + (urllib.quote(event["room_id"]), event["user_id"], event["msg_id"], + self._usr(), feedback_type)) + data = {} + reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data, + alt_text="Sent receipt for %s" % event["msg_id"]) + + def _do_membership_change(self, roomid, membership, userid): + path = "/rooms/%s/state/m.room.member/%s" % (urllib.quote(roomid), urllib.quote(userid)) + data = { + "membership": membership + } + reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) + + def do_displayname(self, line): + """Get or set my displayname: "displayname [new_name]" """ + args = self._parse(line, ["name"]) + path = "/profile/%s/displayname" % (self.config["user"]) + + if "name" in args: + data = {"displayname": args["name"]} + reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) + else: + reactor.callFromThread(self._run_and_pprint, "GET", path) + + def _do_presence_state(self, state, line): + args = self._parse(line, ["msgstring"]) + path = "/presence/%s/status" % (self.config["user"]) + data = {"state": state} + if "msgstring" in args: + data["status_msg"] = args["msgstring"] + + reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) + + def do_offline(self, line): + """Set my presence state to OFFLINE""" + self._do_presence_state(0, line) + + def do_away(self, line): + """Set my presence state to AWAY""" + self._do_presence_state(1, line) + + def do_online(self, line): + """Set my presence state to ONLINE""" + self._do_presence_state(2, line) + + def _parse(self, line, keys, force_keys=False): + """ Parses the given line. + + Args: + line : The line to parse + keys : A list of keys to map onto the args + force_keys : True to enforce that the line has a value for every key + Returns: + A dict of key:arg + """ + line_args = shlex.split(line) + if force_keys and len(line_args) != len(keys): + raise IndexError("Must specify all args: %s" % keys) + + # do $ substitutions + for i, arg in enumerate(line_args): + for config_key in self.config: + if ("$" + config_key) in arg: + arg = arg.replace("$" + config_key, + self.config[config_key]) + line_args[i] = arg + + return dict(zip(keys, line_args)) + + @defer.inlineCallbacks + def _run_and_pprint(self, method, path, data=None, + query_params={"access_token": None}, alt_text=None): + """ Runs an HTTP request and pretty prints the output. + + Args: + method: HTTP method + path: Relative path + data: Raw JSON data if any + query_params: dict of query parameters to add to the url + """ + url = self._url() + path + if "access_token" in query_params: + query_params["access_token"] = self._tok() + + json_res = yield self.http_client.do_request(method, url, + data=data, + qparams=query_params) + if alt_text: + print alt_text + else: + print json.dumps(json_res, indent=4) + + +def save_config(config): + with open(CONFIG_JSON, 'w') as out: + json.dump(config, out) + + +def main(server_url, identity_server_url, username, token, config_path): + print "Synapse command line client" + print "===========================" + print "Server: %s" % server_url + print "Type 'help' to get started." + print "Close this console with CTRL+C then CTRL+D." + if not username or not token: + print "- 'register ' - Register an account" + print "- 'stream' - Connect to the event stream" + print "- 'create ' - Create a room" + print "- 'send ' - Send a message" + http_client = TwistedHttpClient() + + # the command line client + syn_cmd = SynapseCmd(http_client, server_url, identity_server_url, username, token) + + # load synapse.json config from a previous session + global CONFIG_JSON + CONFIG_JSON = config_path # bit cheeky, but just overwrite the global + try: + with open(config_path, 'r') as config: + syn_cmd.config = json.load(config) + try: + http_client.verbose = "on" == syn_cmd.config["verbose"] + except: + pass + print "Loaded config from %s" % config_path + except: + pass + + # Twisted-specific: Runs the command processor in Twisted's event loop + # to maintain a single thread for both commands and event processing. + # If using another HTTP client, just call syn_cmd.cmdloop() + reactor.callInThread(syn_cmd.cmdloop) + reactor.run() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser("Starts a synapse client.") + parser.add_argument( + "-s", "--server", dest="server", default="http://localhost:8008", + help="The URL of the home server to talk to.") + parser.add_argument( + "-i", "--identity-server", dest="identityserver", default="http://localhost:8090", + help="The URL of the identity server to talk to.") + parser.add_argument( + "-u", "--username", dest="username", + help="Your username on the server.") + parser.add_argument( + "-t", "--token", dest="token", + help="Your access token.") + parser.add_argument( + "-c", "--config", dest="config", default=CONFIG_JSON, + help="The location of the config.json file to read from.") + args = parser.parse_args() + + if not args.server: + print "You must supply a server URL to communicate with." + parser.print_help() + sys.exit(1) + + server = args.server + if not server.startswith("http://"): + server = "http://" + args.server + + main(server, args.identityserver, args.username, args.token, args.config) diff --git a/contrib/cmdclient/http.py b/contrib/cmdclient/http.py new file mode 100644 index 0000000000..869f782ec1 --- /dev/null +++ b/contrib/cmdclient/http.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.web.client import Agent, readBody +from twisted.web.http_headers import Headers +from twisted.internet import defer, reactor + +from pprint import pformat + +import json +import urllib + + +class HttpClient(object): + """ Interface for talking json over http + """ + + def put_json(self, url, data): + """ Sends the specifed json data using PUT + + Args: + url (str): The URL to PUT data to. + data (dict): A dict containing the data that will be used as + the request body. This will be encoded as JSON. + + Returns: + Deferred: Succeeds when we get *any* HTTP response. + + The result of the deferred is a tuple of `(code, response)`, + where `response` is a dict representing the decoded JSON body. + """ + pass + + def get_json(self, url, args=None): + """ Get's some json from the given host homeserver and path + + Args: + url (str): The URL to GET data from. + args (dict): A dictionary used to create query strings, defaults to + None. + **Note**: The value of each key is assumed to be an iterable + and *not* a string. + + Returns: + Deferred: Succeeds when we get *any* HTTP response. + + The result of the deferred is a tuple of `(code, response)`, + where `response` is a dict representing the decoded JSON body. + """ + pass + + +class TwistedHttpClient(HttpClient): + """ Wrapper around the twisted HTTP client api. + + Attributes: + agent (twisted.web.client.Agent): The twisted Agent used to send the + requests. + """ + + def __init__(self): + self.agent = Agent(reactor) + + @defer.inlineCallbacks + def put_json(self, url, data): + response = yield self._create_put_request( + url, + data, + headers_dict={"Content-Type": ["application/json"]} + ) + body = yield readBody(response) + defer.returnValue((response.code, body)) + + @defer.inlineCallbacks + def get_json(self, url, args=None): + if args: + # generates a list of strings of form "k=v". + qs = urllib.urlencode(args, True) + url = "%s?%s" % (url, qs) + response = yield self._create_get_request(url) + body = yield readBody(response) + defer.returnValue(json.loads(body)) + + def _create_put_request(self, url, json_data, headers_dict={}): + """ Wrapper of _create_request to issue a PUT request + """ + + if "Content-Type" not in headers_dict: + raise defer.error( + RuntimeError("Must include Content-Type header for PUTs")) + + return self._create_request( + "PUT", + url, + producer=_JsonProducer(json_data), + headers_dict=headers_dict + ) + + def _create_get_request(self, url, headers_dict={}): + """ Wrapper of _create_request to issue a GET request + """ + return self._create_request( + "GET", + url, + headers_dict=headers_dict + ) + + @defer.inlineCallbacks + def do_request(self, method, url, data=None, qparams=None, jsonreq=True, headers={}): + if qparams: + url = "%s?%s" % (url, urllib.urlencode(qparams, True)) + + if jsonreq: + prod = _JsonProducer(data) + headers['Content-Type'] = ["application/json"]; + else: + prod = _RawProducer(data) + + if method in ["POST", "PUT"]: + response = yield self._create_request(method, url, + producer=prod, + headers_dict=headers) + else: + response = yield self._create_request(method, url) + + body = yield readBody(response) + defer.returnValue(json.loads(body)) + + @defer.inlineCallbacks + def _create_request(self, method, url, producer=None, headers_dict={}): + """ Creates and sends a request to the given url + """ + headers_dict["User-Agent"] = ["Synapse Cmd Client"] + + retries_left = 5 + print "%s to %s with headers %s" % (method, url, headers_dict) + if self.verbose and producer: + if "password" in producer.data: + temp = producer.data["password"] + producer.data["password"] = "[REDACTED]" + print json.dumps(producer.data, indent=4) + producer.data["password"] = temp + else: + print json.dumps(producer.data, indent=4) + + while True: + try: + response = yield self.agent.request( + method, + url.encode("UTF8"), + Headers(headers_dict), + producer + ) + break + except Exception as e: + print "uh oh: %s" % e + if retries_left: + yield self.sleep(2 ** (5 - retries_left)) + retries_left -= 1 + else: + raise e + + if self.verbose: + print "Status %s %s" % (response.code, response.phrase) + print pformat(list(response.headers.getAllRawHeaders())) + defer.returnValue(response) + + def sleep(self, seconds): + d = defer.Deferred() + reactor.callLater(seconds, d.callback, seconds) + return d + +class _RawProducer(object): + def __init__(self, data): + self.data = data + self.body = data + self.length = len(self.body) + + def startProducing(self, consumer): + consumer.write(self.body) + return defer.succeed(None) + + def pauseProducing(self): + pass + + def stopProducing(self): + pass + +class _JsonProducer(object): + """ Used by the twisted http client to create the HTTP body from json + """ + def __init__(self, jsn): + self.data = jsn + self.body = json.dumps(jsn).encode("utf8") + self.length = len(self.body) + + def startProducing(self, consumer): + consumer.write(self.body) + return defer.succeed(None) + + def pauseProducing(self): + pass + + def stopProducing(self): + pass \ No newline at end of file diff --git a/contrib/experiments/cursesio.py b/contrib/experiments/cursesio.py new file mode 100644 index 0000000000..95d87a1fda --- /dev/null +++ b/contrib/experiments/cursesio.py @@ -0,0 +1,168 @@ +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import curses +import curses.wrapper +from curses.ascii import isprint + +from twisted.internet import reactor + + +class CursesStdIO(): + def __init__(self, stdscr, callback=None): + self.statusText = "Synapse test app -" + self.searchText = '' + self.stdscr = stdscr + + self.logLine = '' + + self.callback = callback + + self._setup() + + def _setup(self): + self.stdscr.nodelay(1) # Make non blocking + + self.rows, self.cols = self.stdscr.getmaxyx() + self.lines = [] + + curses.use_default_colors() + + self.paintStatus(self.statusText) + self.stdscr.refresh() + + def set_callback(self, callback): + self.callback = callback + + def fileno(self): + """ We want to select on FD 0 """ + return 0 + + def connectionLost(self, reason): + self.close() + + def print_line(self, text): + """ add a line to the internal list of lines""" + + self.lines.append(text) + self.redraw() + + def print_log(self, text): + self.logLine = text + self.redraw() + + def redraw(self): + """ method for redisplaying lines + based on internal list of lines """ + + self.stdscr.clear() + self.paintStatus(self.statusText) + i = 0 + index = len(self.lines) - 1 + while i < (self.rows - 3) and index >= 0: + self.stdscr.addstr(self.rows - 3 - i, 0, self.lines[index], + curses.A_NORMAL) + i = i + 1 + index = index - 1 + + self.printLogLine(self.logLine) + + self.stdscr.refresh() + + def paintStatus(self, text): + if len(text) > self.cols: + raise RuntimeError("TextTooLongError") + + self.stdscr.addstr( + self.rows - 2, 0, + text + ' ' * (self.cols - len(text)), + curses.A_STANDOUT) + + def printLogLine(self, text): + self.stdscr.addstr( + 0, 0, + text + ' ' * (self.cols - len(text)), + curses.A_STANDOUT) + + def doRead(self): + """ Input is ready! """ + curses.noecho() + c = self.stdscr.getch() # read a character + + if c == curses.KEY_BACKSPACE: + self.searchText = self.searchText[:-1] + + elif c == curses.KEY_ENTER or c == 10: + text = self.searchText + self.searchText = '' + + self.print_line(">> %s" % text) + + try: + if self.callback: + self.callback.on_line(text) + except Exception as e: + self.print_line(str(e)) + + self.stdscr.refresh() + + elif isprint(c): + if len(self.searchText) == self.cols - 2: + return + self.searchText = self.searchText + chr(c) + + self.stdscr.addstr(self.rows - 1, 0, + self.searchText + (' ' * ( + self.cols - len(self.searchText) - 2))) + + self.paintStatus(self.statusText + ' %d' % len(self.searchText)) + self.stdscr.move(self.rows - 1, len(self.searchText)) + self.stdscr.refresh() + + def logPrefix(self): + return "CursesStdIO" + + def close(self): + """ clean up """ + + curses.nocbreak() + self.stdscr.keypad(0) + curses.echo() + curses.endwin() + + +class Callback(object): + + def __init__(self, stdio): + self.stdio = stdio + + def on_line(self, text): + self.stdio.print_line(text) + + +def main(stdscr): + screen = CursesStdIO(stdscr) # create Screen object + + callback = Callback(screen) + + screen.set_callback(callback) + + stdscr.refresh() + reactor.addReader(screen) + reactor.run() + screen.close() + + +if __name__ == '__main__': + curses.wrapper(main) diff --git a/contrib/experiments/test_messaging.py b/contrib/experiments/test_messaging.py new file mode 100644 index 0000000000..fedf786cec --- /dev/null +++ b/contrib/experiments/test_messaging.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" This is an example of using the server to server implementation to do a +basic chat style thing. It accepts commands from stdin and outputs to stdout. + +It assumes that ucids are of the form @, and uses as +the address of the remote home server to hit. + +Usage: + python test_messaging.py + +Currently assumes the local address is localhost: + +""" + + +from synapse.federation import ( + ReplicationHandler +) + +from synapse.federation.units import Pdu + +from synapse.util import origin_from_ucid + +from synapse.app.homeserver import SynapseHomeServer + +#from synapse.util.logutils import log_function + +from twisted.internet import reactor, defer +from twisted.python import log + +import argparse +import json +import logging +import os +import re + +import cursesio +import curses.wrapper + + +logger = logging.getLogger("example") + + +def excpetion_errback(failure): + logging.exception(failure) + + +class InputOutput(object): + """ This is responsible for basic I/O so that a user can interact with + the example app. + """ + + def __init__(self, screen, user): + self.screen = screen + self.user = user + + def set_home_server(self, server): + self.server = server + + def on_line(self, line): + """ This is where we process commands. + """ + + try: + m = re.match("^join (\S+)$", line) + if m: + # The `sender` wants to join a room. + room_name, = m.groups() + self.print_line("%s joining %s" % (self.user, room_name)) + self.server.join_room(room_name, self.user, self.user) + #self.print_line("OK.") + return + + m = re.match("^invite (\S+) (\S+)$", line) + if m: + # `sender` wants to invite someone to a room + room_name, invitee = m.groups() + self.print_line("%s invited to %s" % (invitee, room_name)) + self.server.invite_to_room(room_name, self.user, invitee) + #self.print_line("OK.") + return + + m = re.match("^send (\S+) (.*)$", line) + if m: + # `sender` wants to message a room + room_name, body = m.groups() + self.print_line("%s send to %s" % (self.user, room_name)) + self.server.send_message(room_name, self.user, body) + #self.print_line("OK.") + return + + m = re.match("^backfill (\S+)$", line) + if m: + # we want to backfill a room + room_name, = m.groups() + self.print_line("backfill %s" % room_name) + self.server.backfill(room_name) + return + + self.print_line("Unrecognized command") + + except Exception as e: + logger.exception(e) + + def print_line(self, text): + self.screen.print_line(text) + + def print_log(self, text): + self.screen.print_log(text) + + +class IOLoggerHandler(logging.Handler): + + def __init__(self, io): + logging.Handler.__init__(self) + self.io = io + + def emit(self, record): + if record.levelno < logging.WARN: + return + + msg = self.format(record) + self.io.print_log(msg) + + +class Room(object): + """ Used to store (in memory) the current membership state of a room, and + which home servers we should send PDUs associated with the room to. + """ + def __init__(self, room_name): + self.room_name = room_name + self.invited = set() + self.participants = set() + self.servers = set() + + self.oldest_server = None + + self.have_got_metadata = False + + def add_participant(self, participant): + """ Someone has joined the room + """ + self.participants.add(participant) + self.invited.discard(participant) + + server = origin_from_ucid(participant) + self.servers.add(server) + + if not self.oldest_server: + self.oldest_server = server + + def add_invited(self, invitee): + """ Someone has been invited to the room + """ + self.invited.add(invitee) + self.servers.add(origin_from_ucid(invitee)) + + +class HomeServer(ReplicationHandler): + """ A very basic home server implentation that allows people to join a + room and then invite other people. + """ + def __init__(self, server_name, replication_layer, output): + self.server_name = server_name + self.replication_layer = replication_layer + self.replication_layer.set_handler(self) + + self.joined_rooms = {} + + self.output = output + + def on_receive_pdu(self, pdu): + """ We just received a PDU + """ + pdu_type = pdu.pdu_type + + if pdu_type == "sy.room.message": + self._on_message(pdu) + elif pdu_type == "sy.room.member" and "membership" in pdu.content: + if pdu.content["membership"] == "join": + self._on_join(pdu.context, pdu.state_key) + elif pdu.content["membership"] == "invite": + self._on_invite(pdu.origin, pdu.context, pdu.state_key) + else: + self.output.print_line("#%s (unrec) %s = %s" % + (pdu.context, pdu.pdu_type, json.dumps(pdu.content)) + ) + + #def on_state_change(self, pdu): + ##self.output.print_line("#%s (state) %s *** %s" % + ##(pdu.context, pdu.state_key, pdu.pdu_type) + ##) + + #if "joinee" in pdu.content: + #self._on_join(pdu.context, pdu.content["joinee"]) + #elif "invitee" in pdu.content: + #self._on_invite(pdu.origin, pdu.context, pdu.content["invitee"]) + + def _on_message(self, pdu): + """ We received a message + """ + self.output.print_line("#%s %s %s" % + (pdu.context, pdu.content["sender"], pdu.content["body"]) + ) + + def _on_join(self, context, joinee): + """ Someone has joined a room, either a remote user or a local user + """ + room = self._get_or_create_room(context) + room.add_participant(joinee) + + self.output.print_line("#%s %s %s" % + (context, joinee, "*** JOINED") + ) + + def _on_invite(self, origin, context, invitee): + """ Someone has been invited + """ + room = self._get_or_create_room(context) + room.add_invited(invitee) + + self.output.print_line("#%s %s %s" % + (context, invitee, "*** INVITED") + ) + + if not room.have_got_metadata and origin is not self.server_name: + logger.debug("Get room state") + self.replication_layer.get_state_for_context(origin, context) + room.have_got_metadata = True + + @defer.inlineCallbacks + def send_message(self, room_name, sender, body): + """ Send a message to a room! + """ + destinations = yield self.get_servers_for_context(room_name) + + try: + yield self.replication_layer.send_pdu( + Pdu.create_new( + context=room_name, + pdu_type="sy.room.message", + content={"sender": sender, "body": body}, + origin=self.server_name, + destinations=destinations, + ) + ) + except Exception as e: + logger.exception(e) + + @defer.inlineCallbacks + def join_room(self, room_name, sender, joinee): + """ Join a room! + """ + self._on_join(room_name, joinee) + + destinations = yield self.get_servers_for_context(room_name) + + try: + pdu = Pdu.create_new( + context=room_name, + pdu_type="sy.room.member", + is_state=True, + state_key=joinee, + content={"membership": "join"}, + origin=self.server_name, + destinations=destinations, + ) + yield self.replication_layer.send_pdu(pdu) + except Exception as e: + logger.exception(e) + + @defer.inlineCallbacks + def invite_to_room(self, room_name, sender, invitee): + """ Invite someone to a room! + """ + self._on_invite(self.server_name, room_name, invitee) + + destinations = yield self.get_servers_for_context(room_name) + + try: + yield self.replication_layer.send_pdu( + Pdu.create_new( + context=room_name, + is_state=True, + pdu_type="sy.room.member", + state_key=invitee, + content={"membership": "invite"}, + origin=self.server_name, + destinations=destinations, + ) + ) + except Exception as e: + logger.exception(e) + + def backfill(self, room_name, limit=5): + room = self.joined_rooms.get(room_name) + + if not room: + return + + dest = room.oldest_server + + return self.replication_layer.backfill(dest, room_name, limit) + + def _get_room_remote_servers(self, room_name): + return [i for i in self.joined_rooms.setdefault(room_name,).servers] + + def _get_or_create_room(self, room_name): + return self.joined_rooms.setdefault(room_name, Room(room_name)) + + def get_servers_for_context(self, context): + return defer.succeed( + self.joined_rooms.setdefault(context, Room(context)).servers + ) + + +def main(stdscr): + parser = argparse.ArgumentParser() + parser.add_argument('user', type=str) + parser.add_argument('-v', '--verbose', action='count') + args = parser.parse_args() + + user = args.user + server_name = origin_from_ucid(user) + + ## Set up logging ## + + root_logger = logging.getLogger() + + formatter = logging.Formatter('%(asctime)s - %(name)s - %(lineno)d - ' + '%(levelname)s - %(message)s') + if not os.path.exists("logs"): + os.makedirs("logs") + fh = logging.FileHandler("logs/%s" % user) + fh.setFormatter(formatter) + + root_logger.addHandler(fh) + root_logger.setLevel(logging.DEBUG) + + # Hack: The only way to get it to stop logging to sys.stderr :( + log.theLogPublisher.observers = [] + observer = log.PythonLoggingObserver() + observer.start() + + ## Set up synapse server + + curses_stdio = cursesio.CursesStdIO(stdscr) + input_output = InputOutput(curses_stdio, user) + + curses_stdio.set_callback(input_output) + + app_hs = SynapseHomeServer(server_name, db_name="dbs/%s" % user) + replication = app_hs.get_replication_layer() + + hs = HomeServer(server_name, replication, curses_stdio) + + input_output.set_home_server(hs) + + ## Add input_output logger + io_logger = IOLoggerHandler(input_output) + io_logger.setFormatter(formatter) + root_logger.addHandler(io_logger) + + ## Start! ## + + try: + port = int(server_name.split(":")[1]) + except: + port = 12345 + + app_hs.get_http_server().start_listening(port) + + reactor.addReader(curses_stdio) + + reactor.run() + + +if __name__ == "__main__": + curses.wrapper(main) diff --git a/contrib/graph/graph.py b/contrib/graph/graph.py new file mode 100644 index 0000000000..b2acadcf5e --- /dev/null +++ b/contrib/graph/graph.py @@ -0,0 +1,151 @@ +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import sqlite3 +import pydot +import cgi +import json +import datetime +import argparse +import urllib2 + + +def make_name(pdu_id, origin): + return "%s@%s" % (pdu_id, origin) + + +def make_graph(pdus, room, filename_prefix): + pdu_map = {} + node_map = {} + + origins = set() + colors = set(("red", "green", "blue", "yellow", "purple")) + + for pdu in pdus: + origins.add(pdu.get("origin")) + + color_map = {color: color for color in colors if color in origins} + colors -= set(color_map.values()) + + color_map[None] = "black" + + for o in origins: + if o in color_map: + continue + try: + c = colors.pop() + color_map[o] = c + except: + print "Run out of colours!" + color_map[o] = "black" + + graph = pydot.Dot(graph_name="Test") + + for pdu in pdus: + name = make_name(pdu.get("pdu_id"), pdu.get("origin")) + pdu_map[name] = pdu + + t = datetime.datetime.fromtimestamp( + float(pdu["ts"]) / 1000 + ).strftime('%Y-%m-%d %H:%M:%S,%f') + + label = ( + "<" + "%(name)s
" + "Type: %(type)s
" + "State key: %(state_key)s
" + "Content: %(content)s
" + "Time: %(time)s
" + "Depth: %(depth)s
" + ">" + ) % { + "name": name, + "type": pdu.get("pdu_type"), + "state_key": pdu.get("state_key"), + "content": cgi.escape(json.dumps(pdu.get("content")), quote=True), + "time": t, + "depth": pdu.get("depth"), + } + + node = pydot.Node( + name=name, + label=label, + color=color_map[pdu.get("origin")] + ) + node_map[name] = node + graph.add_node(node) + + for pdu in pdus: + start_name = make_name(pdu.get("pdu_id"), pdu.get("origin")) + for i, o in pdu.get("prev_pdus", []): + end_name = make_name(i, o) + + if end_name not in node_map: + print "%s not in nodes" % end_name + continue + + edge = pydot.Edge(node_map[start_name], node_map[end_name]) + graph.add_edge(edge) + + # Add prev_state edges, if they exist + if pdu.get("prev_state_id") and pdu.get("prev_state_origin"): + prev_state_name = make_name( + pdu.get("prev_state_id"), pdu.get("prev_state_origin") + ) + + if prev_state_name in node_map: + state_edge = pydot.Edge( + node_map[start_name], node_map[prev_state_name], + style='dotted' + ) + graph.add_edge(state_edge) + + graph.write('%s.dot' % filename_prefix, format='raw', prog='dot') +# graph.write_png("%s.png" % filename_prefix, prog='dot') + graph.write_svg("%s.svg" % filename_prefix, prog='dot') + + +def get_pdus(host, room): + transaction = json.loads( + urllib2.urlopen( + "http://%s/_matrix/federation/v1/context/%s/" % (host, room) + ).read() + ) + + return transaction["pdus"] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generate a PDU graph for a given room by talking " + "to the given homeserver to get the list of PDUs. \n" + "Requires pydot." + ) + parser.add_argument( + "-p", "--prefix", dest="prefix", + help="String to prefix output files with" + ) + parser.add_argument('host') + parser.add_argument('room') + + args = parser.parse_args() + + host = args.host + room = args.room + prefix = args.prefix if args.prefix else "%s_graph" % (room) + + pdus = get_pdus(host, room) + + make_graph(pdus, room, prefix) diff --git a/contrib/graph/graph2.py b/contrib/graph/graph2.py new file mode 100644 index 0000000000..6b551d42e5 --- /dev/null +++ b/contrib/graph/graph2.py @@ -0,0 +1,156 @@ +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import sqlite3 +import pydot +import cgi +import json +import datetime +import argparse + +from synapse.events import FrozenEvent + + +def make_graph(db_name, room_id, file_prefix, limit): + conn = sqlite3.connect(db_name) + + sql = ( + "SELECT json FROM event_json as j " + "INNER JOIN events as e ON e.event_id = j.event_id " + "WHERE j.room_id = ?" + ) + + args = [room_id] + + if limit: + sql += ( + " ORDER BY topological_ordering DESC, stream_ordering DESC " + "LIMIT ?" + ) + + args.append(limit) + + c = conn.execute(sql, args) + + events = [FrozenEvent(json.loads(e[0])) for e in c.fetchall()] + + events.sort(key=lambda e: e.depth) + + node_map = {} + state_groups = {} + + graph = pydot.Dot(graph_name="Test") + + for event in events: + c = conn.execute( + "SELECT state_group FROM event_to_state_groups " + "WHERE event_id = ?", + (event.event_id,) + ) + + res = c.fetchone() + state_group = res[0] if res else None + + if state_group is not None: + state_groups.setdefault(state_group, []).append(event.event_id) + + t = datetime.datetime.fromtimestamp( + float(event.origin_server_ts) / 1000 + ).strftime('%Y-%m-%d %H:%M:%S,%f') + + content = json.dumps(event.get_dict()["content"]) + + label = ( + "<" + "%(name)s
" + "Type: %(type)s
" + "State key: %(state_key)s
" + "Content: %(content)s
" + "Time: %(time)s
" + "Depth: %(depth)s
" + "State group: %(state_group)s
" + ">" + ) % { + "name": event.event_id, + "type": event.type, + "state_key": event.get("state_key", None), + "content": cgi.escape(content, quote=True), + "time": t, + "depth": event.depth, + "state_group": state_group, + } + + node = pydot.Node( + name=event.event_id, + label=label, + ) + + node_map[event.event_id] = node + graph.add_node(node) + + for event in events: + for prev_id, _ in event.prev_events: + try: + end_node = node_map[prev_id] + except: + end_node = pydot.Node( + name=prev_id, + label="<%s>" % (prev_id,), + ) + + node_map[prev_id] = end_node + graph.add_node(end_node) + + edge = pydot.Edge(node_map[event.event_id], end_node) + graph.add_edge(edge) + + for group, event_ids in state_groups.items(): + if len(event_ids) <= 1: + continue + + cluster = pydot.Cluster( + str(group), + label="" % (str(group),) + ) + + for event_id in event_ids: + cluster.add_node(node_map[event_id]) + + graph.add_subgraph(cluster) + + graph.write('%s.dot' % file_prefix, format='raw', prog='dot') + graph.write_svg("%s.svg" % file_prefix, prog='dot') + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generate a PDU graph for a given room by talking " + "to the given homeserver to get the list of PDUs. \n" + "Requires pydot." + ) + parser.add_argument( + "-p", "--prefix", dest="prefix", + help="String to prefix output files with", + default="graph_output" + ) + parser.add_argument( + "-l", "--limit", + help="Only retrieve the last N events.", + ) + parser.add_argument('db') + parser.add_argument('room') + + args = parser.parse_args() + + make_graph(args.db, args.room, args.prefix, args.limit) diff --git a/experiments/cursesio.py b/experiments/cursesio.py deleted file mode 100644 index 95d87a1fda..0000000000 --- a/experiments/cursesio.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import curses -import curses.wrapper -from curses.ascii import isprint - -from twisted.internet import reactor - - -class CursesStdIO(): - def __init__(self, stdscr, callback=None): - self.statusText = "Synapse test app -" - self.searchText = '' - self.stdscr = stdscr - - self.logLine = '' - - self.callback = callback - - self._setup() - - def _setup(self): - self.stdscr.nodelay(1) # Make non blocking - - self.rows, self.cols = self.stdscr.getmaxyx() - self.lines = [] - - curses.use_default_colors() - - self.paintStatus(self.statusText) - self.stdscr.refresh() - - def set_callback(self, callback): - self.callback = callback - - def fileno(self): - """ We want to select on FD 0 """ - return 0 - - def connectionLost(self, reason): - self.close() - - def print_line(self, text): - """ add a line to the internal list of lines""" - - self.lines.append(text) - self.redraw() - - def print_log(self, text): - self.logLine = text - self.redraw() - - def redraw(self): - """ method for redisplaying lines - based on internal list of lines """ - - self.stdscr.clear() - self.paintStatus(self.statusText) - i = 0 - index = len(self.lines) - 1 - while i < (self.rows - 3) and index >= 0: - self.stdscr.addstr(self.rows - 3 - i, 0, self.lines[index], - curses.A_NORMAL) - i = i + 1 - index = index - 1 - - self.printLogLine(self.logLine) - - self.stdscr.refresh() - - def paintStatus(self, text): - if len(text) > self.cols: - raise RuntimeError("TextTooLongError") - - self.stdscr.addstr( - self.rows - 2, 0, - text + ' ' * (self.cols - len(text)), - curses.A_STANDOUT) - - def printLogLine(self, text): - self.stdscr.addstr( - 0, 0, - text + ' ' * (self.cols - len(text)), - curses.A_STANDOUT) - - def doRead(self): - """ Input is ready! """ - curses.noecho() - c = self.stdscr.getch() # read a character - - if c == curses.KEY_BACKSPACE: - self.searchText = self.searchText[:-1] - - elif c == curses.KEY_ENTER or c == 10: - text = self.searchText - self.searchText = '' - - self.print_line(">> %s" % text) - - try: - if self.callback: - self.callback.on_line(text) - except Exception as e: - self.print_line(str(e)) - - self.stdscr.refresh() - - elif isprint(c): - if len(self.searchText) == self.cols - 2: - return - self.searchText = self.searchText + chr(c) - - self.stdscr.addstr(self.rows - 1, 0, - self.searchText + (' ' * ( - self.cols - len(self.searchText) - 2))) - - self.paintStatus(self.statusText + ' %d' % len(self.searchText)) - self.stdscr.move(self.rows - 1, len(self.searchText)) - self.stdscr.refresh() - - def logPrefix(self): - return "CursesStdIO" - - def close(self): - """ clean up """ - - curses.nocbreak() - self.stdscr.keypad(0) - curses.echo() - curses.endwin() - - -class Callback(object): - - def __init__(self, stdio): - self.stdio = stdio - - def on_line(self, text): - self.stdio.print_line(text) - - -def main(stdscr): - screen = CursesStdIO(stdscr) # create Screen object - - callback = Callback(screen) - - screen.set_callback(callback) - - stdscr.refresh() - reactor.addReader(screen) - reactor.run() - screen.close() - - -if __name__ == '__main__': - curses.wrapper(main) diff --git a/experiments/test_messaging.py b/experiments/test_messaging.py deleted file mode 100644 index fedf786cec..0000000000 --- a/experiments/test_messaging.py +++ /dev/null @@ -1,394 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -""" This is an example of using the server to server implementation to do a -basic chat style thing. It accepts commands from stdin and outputs to stdout. - -It assumes that ucids are of the form @, and uses as -the address of the remote home server to hit. - -Usage: - python test_messaging.py - -Currently assumes the local address is localhost: - -""" - - -from synapse.federation import ( - ReplicationHandler -) - -from synapse.federation.units import Pdu - -from synapse.util import origin_from_ucid - -from synapse.app.homeserver import SynapseHomeServer - -#from synapse.util.logutils import log_function - -from twisted.internet import reactor, defer -from twisted.python import log - -import argparse -import json -import logging -import os -import re - -import cursesio -import curses.wrapper - - -logger = logging.getLogger("example") - - -def excpetion_errback(failure): - logging.exception(failure) - - -class InputOutput(object): - """ This is responsible for basic I/O so that a user can interact with - the example app. - """ - - def __init__(self, screen, user): - self.screen = screen - self.user = user - - def set_home_server(self, server): - self.server = server - - def on_line(self, line): - """ This is where we process commands. - """ - - try: - m = re.match("^join (\S+)$", line) - if m: - # The `sender` wants to join a room. - room_name, = m.groups() - self.print_line("%s joining %s" % (self.user, room_name)) - self.server.join_room(room_name, self.user, self.user) - #self.print_line("OK.") - return - - m = re.match("^invite (\S+) (\S+)$", line) - if m: - # `sender` wants to invite someone to a room - room_name, invitee = m.groups() - self.print_line("%s invited to %s" % (invitee, room_name)) - self.server.invite_to_room(room_name, self.user, invitee) - #self.print_line("OK.") - return - - m = re.match("^send (\S+) (.*)$", line) - if m: - # `sender` wants to message a room - room_name, body = m.groups() - self.print_line("%s send to %s" % (self.user, room_name)) - self.server.send_message(room_name, self.user, body) - #self.print_line("OK.") - return - - m = re.match("^backfill (\S+)$", line) - if m: - # we want to backfill a room - room_name, = m.groups() - self.print_line("backfill %s" % room_name) - self.server.backfill(room_name) - return - - self.print_line("Unrecognized command") - - except Exception as e: - logger.exception(e) - - def print_line(self, text): - self.screen.print_line(text) - - def print_log(self, text): - self.screen.print_log(text) - - -class IOLoggerHandler(logging.Handler): - - def __init__(self, io): - logging.Handler.__init__(self) - self.io = io - - def emit(self, record): - if record.levelno < logging.WARN: - return - - msg = self.format(record) - self.io.print_log(msg) - - -class Room(object): - """ Used to store (in memory) the current membership state of a room, and - which home servers we should send PDUs associated with the room to. - """ - def __init__(self, room_name): - self.room_name = room_name - self.invited = set() - self.participants = set() - self.servers = set() - - self.oldest_server = None - - self.have_got_metadata = False - - def add_participant(self, participant): - """ Someone has joined the room - """ - self.participants.add(participant) - self.invited.discard(participant) - - server = origin_from_ucid(participant) - self.servers.add(server) - - if not self.oldest_server: - self.oldest_server = server - - def add_invited(self, invitee): - """ Someone has been invited to the room - """ - self.invited.add(invitee) - self.servers.add(origin_from_ucid(invitee)) - - -class HomeServer(ReplicationHandler): - """ A very basic home server implentation that allows people to join a - room and then invite other people. - """ - def __init__(self, server_name, replication_layer, output): - self.server_name = server_name - self.replication_layer = replication_layer - self.replication_layer.set_handler(self) - - self.joined_rooms = {} - - self.output = output - - def on_receive_pdu(self, pdu): - """ We just received a PDU - """ - pdu_type = pdu.pdu_type - - if pdu_type == "sy.room.message": - self._on_message(pdu) - elif pdu_type == "sy.room.member" and "membership" in pdu.content: - if pdu.content["membership"] == "join": - self._on_join(pdu.context, pdu.state_key) - elif pdu.content["membership"] == "invite": - self._on_invite(pdu.origin, pdu.context, pdu.state_key) - else: - self.output.print_line("#%s (unrec) %s = %s" % - (pdu.context, pdu.pdu_type, json.dumps(pdu.content)) - ) - - #def on_state_change(self, pdu): - ##self.output.print_line("#%s (state) %s *** %s" % - ##(pdu.context, pdu.state_key, pdu.pdu_type) - ##) - - #if "joinee" in pdu.content: - #self._on_join(pdu.context, pdu.content["joinee"]) - #elif "invitee" in pdu.content: - #self._on_invite(pdu.origin, pdu.context, pdu.content["invitee"]) - - def _on_message(self, pdu): - """ We received a message - """ - self.output.print_line("#%s %s %s" % - (pdu.context, pdu.content["sender"], pdu.content["body"]) - ) - - def _on_join(self, context, joinee): - """ Someone has joined a room, either a remote user or a local user - """ - room = self._get_or_create_room(context) - room.add_participant(joinee) - - self.output.print_line("#%s %s %s" % - (context, joinee, "*** JOINED") - ) - - def _on_invite(self, origin, context, invitee): - """ Someone has been invited - """ - room = self._get_or_create_room(context) - room.add_invited(invitee) - - self.output.print_line("#%s %s %s" % - (context, invitee, "*** INVITED") - ) - - if not room.have_got_metadata and origin is not self.server_name: - logger.debug("Get room state") - self.replication_layer.get_state_for_context(origin, context) - room.have_got_metadata = True - - @defer.inlineCallbacks - def send_message(self, room_name, sender, body): - """ Send a message to a room! - """ - destinations = yield self.get_servers_for_context(room_name) - - try: - yield self.replication_layer.send_pdu( - Pdu.create_new( - context=room_name, - pdu_type="sy.room.message", - content={"sender": sender, "body": body}, - origin=self.server_name, - destinations=destinations, - ) - ) - except Exception as e: - logger.exception(e) - - @defer.inlineCallbacks - def join_room(self, room_name, sender, joinee): - """ Join a room! - """ - self._on_join(room_name, joinee) - - destinations = yield self.get_servers_for_context(room_name) - - try: - pdu = Pdu.create_new( - context=room_name, - pdu_type="sy.room.member", - is_state=True, - state_key=joinee, - content={"membership": "join"}, - origin=self.server_name, - destinations=destinations, - ) - yield self.replication_layer.send_pdu(pdu) - except Exception as e: - logger.exception(e) - - @defer.inlineCallbacks - def invite_to_room(self, room_name, sender, invitee): - """ Invite someone to a room! - """ - self._on_invite(self.server_name, room_name, invitee) - - destinations = yield self.get_servers_for_context(room_name) - - try: - yield self.replication_layer.send_pdu( - Pdu.create_new( - context=room_name, - is_state=True, - pdu_type="sy.room.member", - state_key=invitee, - content={"membership": "invite"}, - origin=self.server_name, - destinations=destinations, - ) - ) - except Exception as e: - logger.exception(e) - - def backfill(self, room_name, limit=5): - room = self.joined_rooms.get(room_name) - - if not room: - return - - dest = room.oldest_server - - return self.replication_layer.backfill(dest, room_name, limit) - - def _get_room_remote_servers(self, room_name): - return [i for i in self.joined_rooms.setdefault(room_name,).servers] - - def _get_or_create_room(self, room_name): - return self.joined_rooms.setdefault(room_name, Room(room_name)) - - def get_servers_for_context(self, context): - return defer.succeed( - self.joined_rooms.setdefault(context, Room(context)).servers - ) - - -def main(stdscr): - parser = argparse.ArgumentParser() - parser.add_argument('user', type=str) - parser.add_argument('-v', '--verbose', action='count') - args = parser.parse_args() - - user = args.user - server_name = origin_from_ucid(user) - - ## Set up logging ## - - root_logger = logging.getLogger() - - formatter = logging.Formatter('%(asctime)s - %(name)s - %(lineno)d - ' - '%(levelname)s - %(message)s') - if not os.path.exists("logs"): - os.makedirs("logs") - fh = logging.FileHandler("logs/%s" % user) - fh.setFormatter(formatter) - - root_logger.addHandler(fh) - root_logger.setLevel(logging.DEBUG) - - # Hack: The only way to get it to stop logging to sys.stderr :( - log.theLogPublisher.observers = [] - observer = log.PythonLoggingObserver() - observer.start() - - ## Set up synapse server - - curses_stdio = cursesio.CursesStdIO(stdscr) - input_output = InputOutput(curses_stdio, user) - - curses_stdio.set_callback(input_output) - - app_hs = SynapseHomeServer(server_name, db_name="dbs/%s" % user) - replication = app_hs.get_replication_layer() - - hs = HomeServer(server_name, replication, curses_stdio) - - input_output.set_home_server(hs) - - ## Add input_output logger - io_logger = IOLoggerHandler(input_output) - io_logger.setFormatter(formatter) - root_logger.addHandler(io_logger) - - ## Start! ## - - try: - port = int(server_name.split(":")[1]) - except: - port = 12345 - - app_hs.get_http_server().start_listening(port) - - reactor.addReader(curses_stdio) - - reactor.run() - - -if __name__ == "__main__": - curses.wrapper(main) diff --git a/graph/graph.py b/graph/graph.py deleted file mode 100644 index b2acadcf5e..0000000000 --- a/graph/graph.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import sqlite3 -import pydot -import cgi -import json -import datetime -import argparse -import urllib2 - - -def make_name(pdu_id, origin): - return "%s@%s" % (pdu_id, origin) - - -def make_graph(pdus, room, filename_prefix): - pdu_map = {} - node_map = {} - - origins = set() - colors = set(("red", "green", "blue", "yellow", "purple")) - - for pdu in pdus: - origins.add(pdu.get("origin")) - - color_map = {color: color for color in colors if color in origins} - colors -= set(color_map.values()) - - color_map[None] = "black" - - for o in origins: - if o in color_map: - continue - try: - c = colors.pop() - color_map[o] = c - except: - print "Run out of colours!" - color_map[o] = "black" - - graph = pydot.Dot(graph_name="Test") - - for pdu in pdus: - name = make_name(pdu.get("pdu_id"), pdu.get("origin")) - pdu_map[name] = pdu - - t = datetime.datetime.fromtimestamp( - float(pdu["ts"]) / 1000 - ).strftime('%Y-%m-%d %H:%M:%S,%f') - - label = ( - "<" - "%(name)s
" - "Type: %(type)s
" - "State key: %(state_key)s
" - "Content: %(content)s
" - "Time: %(time)s
" - "Depth: %(depth)s
" - ">" - ) % { - "name": name, - "type": pdu.get("pdu_type"), - "state_key": pdu.get("state_key"), - "content": cgi.escape(json.dumps(pdu.get("content")), quote=True), - "time": t, - "depth": pdu.get("depth"), - } - - node = pydot.Node( - name=name, - label=label, - color=color_map[pdu.get("origin")] - ) - node_map[name] = node - graph.add_node(node) - - for pdu in pdus: - start_name = make_name(pdu.get("pdu_id"), pdu.get("origin")) - for i, o in pdu.get("prev_pdus", []): - end_name = make_name(i, o) - - if end_name not in node_map: - print "%s not in nodes" % end_name - continue - - edge = pydot.Edge(node_map[start_name], node_map[end_name]) - graph.add_edge(edge) - - # Add prev_state edges, if they exist - if pdu.get("prev_state_id") and pdu.get("prev_state_origin"): - prev_state_name = make_name( - pdu.get("prev_state_id"), pdu.get("prev_state_origin") - ) - - if prev_state_name in node_map: - state_edge = pydot.Edge( - node_map[start_name], node_map[prev_state_name], - style='dotted' - ) - graph.add_edge(state_edge) - - graph.write('%s.dot' % filename_prefix, format='raw', prog='dot') -# graph.write_png("%s.png" % filename_prefix, prog='dot') - graph.write_svg("%s.svg" % filename_prefix, prog='dot') - - -def get_pdus(host, room): - transaction = json.loads( - urllib2.urlopen( - "http://%s/_matrix/federation/v1/context/%s/" % (host, room) - ).read() - ) - - return transaction["pdus"] - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Generate a PDU graph for a given room by talking " - "to the given homeserver to get the list of PDUs. \n" - "Requires pydot." - ) - parser.add_argument( - "-p", "--prefix", dest="prefix", - help="String to prefix output files with" - ) - parser.add_argument('host') - parser.add_argument('room') - - args = parser.parse_args() - - host = args.host - room = args.room - prefix = args.prefix if args.prefix else "%s_graph" % (room) - - pdus = get_pdus(host, room) - - make_graph(pdus, room, prefix) diff --git a/graph/graph2.py b/graph/graph2.py deleted file mode 100644 index 6b551d42e5..0000000000 --- a/graph/graph2.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import sqlite3 -import pydot -import cgi -import json -import datetime -import argparse - -from synapse.events import FrozenEvent - - -def make_graph(db_name, room_id, file_prefix, limit): - conn = sqlite3.connect(db_name) - - sql = ( - "SELECT json FROM event_json as j " - "INNER JOIN events as e ON e.event_id = j.event_id " - "WHERE j.room_id = ?" - ) - - args = [room_id] - - if limit: - sql += ( - " ORDER BY topological_ordering DESC, stream_ordering DESC " - "LIMIT ?" - ) - - args.append(limit) - - c = conn.execute(sql, args) - - events = [FrozenEvent(json.loads(e[0])) for e in c.fetchall()] - - events.sort(key=lambda e: e.depth) - - node_map = {} - state_groups = {} - - graph = pydot.Dot(graph_name="Test") - - for event in events: - c = conn.execute( - "SELECT state_group FROM event_to_state_groups " - "WHERE event_id = ?", - (event.event_id,) - ) - - res = c.fetchone() - state_group = res[0] if res else None - - if state_group is not None: - state_groups.setdefault(state_group, []).append(event.event_id) - - t = datetime.datetime.fromtimestamp( - float(event.origin_server_ts) / 1000 - ).strftime('%Y-%m-%d %H:%M:%S,%f') - - content = json.dumps(event.get_dict()["content"]) - - label = ( - "<" - "%(name)s
" - "Type: %(type)s
" - "State key: %(state_key)s
" - "Content: %(content)s
" - "Time: %(time)s
" - "Depth: %(depth)s
" - "State group: %(state_group)s
" - ">" - ) % { - "name": event.event_id, - "type": event.type, - "state_key": event.get("state_key", None), - "content": cgi.escape(content, quote=True), - "time": t, - "depth": event.depth, - "state_group": state_group, - } - - node = pydot.Node( - name=event.event_id, - label=label, - ) - - node_map[event.event_id] = node - graph.add_node(node) - - for event in events: - for prev_id, _ in event.prev_events: - try: - end_node = node_map[prev_id] - except: - end_node = pydot.Node( - name=prev_id, - label="<%s>" % (prev_id,), - ) - - node_map[prev_id] = end_node - graph.add_node(end_node) - - edge = pydot.Edge(node_map[event.event_id], end_node) - graph.add_edge(edge) - - for group, event_ids in state_groups.items(): - if len(event_ids) <= 1: - continue - - cluster = pydot.Cluster( - str(group), - label="" % (str(group),) - ) - - for event_id in event_ids: - cluster.add_node(node_map[event_id]) - - graph.add_subgraph(cluster) - - graph.write('%s.dot' % file_prefix, format='raw', prog='dot') - graph.write_svg("%s.svg" % file_prefix, prog='dot') - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Generate a PDU graph for a given room by talking " - "to the given homeserver to get the list of PDUs. \n" - "Requires pydot." - ) - parser.add_argument( - "-p", "--prefix", dest="prefix", - help="String to prefix output files with", - default="graph_output" - ) - parser.add_argument( - "-l", "--limit", - help="Only retrieve the last N events.", - ) - parser.add_argument('db') - parser.add_argument('room') - - args = parser.parse_args() - - make_graph(args.db, args.room, args.prefix, args.limit) -- cgit 1.5.1 From 58691680b8dd93fab56fed914b0204f2b6751b6a Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 22 Jan 2015 14:20:53 +0000 Subject: update .gitignore, set media-store-path in demo --- .gitignore | 11 +++-------- demo/start.sh | 3 ++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index af90668c89..2ed22b1cd3 100644 --- a/.gitignore +++ b/.gitignore @@ -26,17 +26,12 @@ htmlcov demo/*.db demo/*.log +demo/*.log.* demo/*.pid +demo/media_store.* demo/etc -graph/*.svg -graph/*.png -graph/*.dot - -**/webclient/config.js -**/webclient/test/coverage/ -**/webclient/test/environment-protractor.js - uploads .idea/ +media_store/ diff --git a/demo/start.sh b/demo/start.sh index ce3e292486..bb2248770d 100755 --- a/demo/start.sh +++ b/demo/start.sh @@ -32,7 +32,8 @@ for port in 8080 8081 8082; do -D --pid-file "$DIR/$port.pid" \ --manhole $((port + 1000)) \ --tls-dh-params-path "demo/demo.tls.dh" \ - $PARAMS $SYNAPSE_PARAMS + --media-store-path "demo/media_store.$port" \ + $PARAMS $SYNAPSE_PARAMS \ python -m synapse.app.homeserver \ --config-path "demo/etc/$port.config" \ -- cgit 1.5.1 From d00cca11b961f59c590956691b750eb082160edc Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 22 Jan 2015 14:28:07 +0000 Subject: Add demo and scripts to python manifest --- MANIFEST.in | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index a1a77ff540..7979ec0809 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,13 @@ +include synctl +include LICENSE +include VERSION +include *.rst + +recursive-include synapse/storage/schema *.sql + +recursive-include demo *.dh +recursive-include demo *.py +recursive-include demo *.sh recursive-include docs * +recursive-include scripts * recursive-include tests *.py -recursive-include synapse/storage/schema *.sql -recursive-include syweb/webclient * -- cgit 1.5.1 From 16bfabb9c51e4721851a5d3220d5b78cff4d7892 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 22 Jan 2015 14:32:51 +0000 Subject: Fix manifest. Ignore contrib and docs directories when checking manifest against source control. --- MANIFEST.in | 1 + setup.cfg | 8 ++++++++ tests/storage/TESTS_NEEDED_FOR | 5 ----- 3 files changed, 9 insertions(+), 5 deletions(-) delete mode 100644 tests/storage/TESTS_NEEDED_FOR diff --git a/MANIFEST.in b/MANIFEST.in index 7979ec0809..8243a942ee 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include synctl include LICENSE include VERSION include *.rst +include demo/README recursive-include synapse/storage/schema *.sql diff --git a/setup.cfg b/setup.cfg index 2830831f00..888ad6ed4a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,3 +8,11 @@ test = trial [trial] test_suite = tests + +[check-manifest] +ignore = + contrib + contrib/* + docs/* + pylint.cfg + tox.ini diff --git a/tests/storage/TESTS_NEEDED_FOR b/tests/storage/TESTS_NEEDED_FOR deleted file mode 100644 index 8e5d0cbdc4..0000000000 --- a/tests/storage/TESTS_NEEDED_FOR +++ /dev/null @@ -1,5 +0,0 @@ -synapse/storage/feedback.py -synapse/storage/keys.py -synapse/storage/pdu.py -synapse/storage/stream.py -synapse/storage/transactions.py -- cgit 1.5.1 From 1d2016b4a881538aa86f4824f1131dfada186ae0 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 22 Jan 2015 14:59:08 +0000 Subject: Move client v1 api rest servlets into a "client/v1" directory --- synapse/client/__init__.py | 14 + synapse/client/v1/__init__.py | 47 ++ synapse/client/v1/admin.py | 47 ++ synapse/client/v1/base.py | 80 +++ synapse/client/v1/directory.py | 112 ++++ synapse/client/v1/events.py | 81 +++ synapse/client/v1/initial_sync.py | 44 ++ synapse/client/v1/login.py | 109 ++++ synapse/client/v1/presence.py | 145 +++++ synapse/client/v1/profile.py | 113 ++++ synapse/client/v1/register.py | 291 ++++++++++ synapse/client/v1/room.py | 559 +++++++++++++++++++ synapse/client/v1/transactions.py | 95 ++++ synapse/client/v1/voip.py | 60 +++ synapse/rest/__init__.py | 47 -- synapse/rest/admin.py | 47 -- synapse/rest/base.py | 80 --- synapse/rest/directory.py | 112 ---- synapse/rest/events.py | 81 --- synapse/rest/initial_sync.py | 44 -- synapse/rest/login.py | 109 ---- synapse/rest/presence.py | 145 ----- synapse/rest/profile.py | 113 ---- synapse/rest/register.py | 291 ---------- synapse/rest/room.py | 559 ------------------- synapse/rest/transactions.py | 95 ---- synapse/rest/voip.py | 60 --- synapse/server.py | 2 +- tests/client/__init__.py | 14 + tests/client/v1/__init__.py | 15 + tests/client/v1/test_events.py | 225 ++++++++ tests/client/v1/test_presence.py | 372 +++++++++++++ tests/client/v1/test_profile.py | 150 ++++++ tests/client/v1/test_rooms.py | 1068 +++++++++++++++++++++++++++++++++++++ tests/client/v1/test_typing.py | 164 ++++++ tests/client/v1/utils.py | 134 +++++ tests/rest/__init__.py | 15 - tests/rest/test_events.py | 225 -------- tests/rest/test_presence.py | 372 ------------- tests/rest/test_profile.py | 150 ------ tests/rest/test_rooms.py | 1068 ------------------------------------- tests/rest/test_typing.py | 164 ------ tests/rest/utils.py | 134 ----- 43 files changed, 3940 insertions(+), 3912 deletions(-) create mode 100644 synapse/client/__init__.py create mode 100644 synapse/client/v1/__init__.py create mode 100644 synapse/client/v1/admin.py create mode 100644 synapse/client/v1/base.py create mode 100644 synapse/client/v1/directory.py create mode 100644 synapse/client/v1/events.py create mode 100644 synapse/client/v1/initial_sync.py create mode 100644 synapse/client/v1/login.py create mode 100644 synapse/client/v1/presence.py create mode 100644 synapse/client/v1/profile.py create mode 100644 synapse/client/v1/register.py create mode 100644 synapse/client/v1/room.py create mode 100644 synapse/client/v1/transactions.py create mode 100644 synapse/client/v1/voip.py delete mode 100644 synapse/rest/__init__.py delete mode 100644 synapse/rest/admin.py delete mode 100644 synapse/rest/base.py delete mode 100644 synapse/rest/directory.py delete mode 100644 synapse/rest/events.py delete mode 100644 synapse/rest/initial_sync.py delete mode 100644 synapse/rest/login.py delete mode 100644 synapse/rest/presence.py delete mode 100644 synapse/rest/profile.py delete mode 100644 synapse/rest/register.py delete mode 100644 synapse/rest/room.py delete mode 100644 synapse/rest/transactions.py delete mode 100644 synapse/rest/voip.py create mode 100644 tests/client/__init__.py create mode 100644 tests/client/v1/__init__.py create mode 100644 tests/client/v1/test_events.py create mode 100644 tests/client/v1/test_presence.py create mode 100644 tests/client/v1/test_profile.py create mode 100644 tests/client/v1/test_rooms.py create mode 100644 tests/client/v1/test_typing.py create mode 100644 tests/client/v1/utils.py delete mode 100644 tests/rest/__init__.py delete mode 100644 tests/rest/test_events.py delete mode 100644 tests/rest/test_presence.py delete mode 100644 tests/rest/test_profile.py delete mode 100644 tests/rest/test_rooms.py delete mode 100644 tests/rest/test_typing.py delete mode 100644 tests/rest/utils.py diff --git a/synapse/client/__init__.py b/synapse/client/__init__.py new file mode 100644 index 0000000000..1a84d94cd9 --- /dev/null +++ b/synapse/client/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/synapse/client/v1/__init__.py b/synapse/client/v1/__init__.py new file mode 100644 index 0000000000..88ec9cd27d --- /dev/null +++ b/synapse/client/v1/__init__.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from . import ( + room, events, register, login, profile, presence, initial_sync, directory, + voip, admin, +) + + +class RestServletFactory(object): + + """ A factory for creating REST servlets. + + These REST servlets represent the entire client-server REST API. Generally + speaking, they serve as wrappers around events and the handlers that + process them. + + See synapse.events for information on synapse events. + """ + + def __init__(self, hs): + client_resource = hs.get_resource_for_client() + + # TODO(erikj): There *must* be a better way of doing this. + room.register_servlets(hs, client_resource) + events.register_servlets(hs, client_resource) + register.register_servlets(hs, client_resource) + login.register_servlets(hs, client_resource) + profile.register_servlets(hs, client_resource) + presence.register_servlets(hs, client_resource) + initial_sync.register_servlets(hs, client_resource) + directory.register_servlets(hs, client_resource) + voip.register_servlets(hs, client_resource) + admin.register_servlets(hs, client_resource) diff --git a/synapse/client/v1/admin.py b/synapse/client/v1/admin.py new file mode 100644 index 0000000000..0aa83514c8 --- /dev/null +++ b/synapse/client/v1/admin.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from synapse.api.errors import AuthError, SynapseError +from base import RestServlet, client_path_pattern + +import logging + +logger = logging.getLogger(__name__) + + +class WhoisRestServlet(RestServlet): + PATTERN = client_path_pattern("/admin/whois/(?P[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + target_user = self.hs.parse_userid(user_id) + auth_user = yield self.auth.get_user_by_req(request) + is_admin = yield self.auth.is_server_admin(auth_user) + + if not is_admin and target_user != auth_user: + raise AuthError(403, "You are not a server admin") + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only whois a local user") + + ret = yield self.handlers.admin_handler.get_whois(target_user) + + defer.returnValue((200, ret)) + + +def register_servlets(hs, http_server): + WhoisRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/base.py b/synapse/client/v1/base.py new file mode 100644 index 0000000000..d005206b77 --- /dev/null +++ b/synapse/client/v1/base.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains base REST classes for constructing REST servlets. """ +from synapse.api.urls import CLIENT_PREFIX +from .transactions import HttpTransactionStore +import re + +import logging + + +logger = logging.getLogger(__name__) + + +def client_path_pattern(path_regex): + """Creates a regex compiled client path with the correct client path + prefix. + + Args: + path_regex (str): The regex string to match. This should NOT have a ^ + as this will be prefixed. + Returns: + SRE_Pattern + """ + return re.compile("^" + CLIENT_PREFIX + path_regex) + + +class RestServlet(object): + + """ A Synapse REST Servlet. + + An implementing class can either provide its own custom 'register' method, + or use the automatic pattern handling provided by the base class. + + To use this latter, the implementing class instead provides a `PATTERN` + class attribute containing a pre-compiled regular expression. The automatic + register method will then use this method to register any of the following + instance methods associated with the corresponding HTTP method: + + on_GET + on_PUT + on_POST + on_DELETE + on_OPTIONS + + Automatically handles turning CodeMessageExceptions thrown by these methods + into the appropriate HTTP response. + """ + + def __init__(self, hs): + self.hs = hs + + self.handlers = hs.get_handlers() + self.builder_factory = hs.get_event_builder_factory() + self.auth = hs.get_auth() + self.txns = HttpTransactionStore() + + def register(self, http_server): + """ Register this servlet with the given HTTP server. """ + if hasattr(self, "PATTERN"): + pattern = self.PATTERN + + for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"): + if hasattr(self, "on_%s" % (method)): + method_handler = getattr(self, "on_%s" % (method)) + http_server.register_path(method, pattern, method_handler) + else: + raise NotImplementedError("RestServlet must register something.") diff --git a/synapse/client/v1/directory.py b/synapse/client/v1/directory.py new file mode 100644 index 0000000000..7ff44fdd9e --- /dev/null +++ b/synapse/client/v1/directory.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from twisted.internet import defer + +from synapse.api.errors import AuthError, SynapseError, Codes +from base import RestServlet, client_path_pattern + +import json +import logging + + +logger = logging.getLogger(__name__) + + +def register_servlets(hs, http_server): + ClientDirectoryServer(hs).register(http_server) + + +class ClientDirectoryServer(RestServlet): + PATTERN = client_path_pattern("/directory/room/(?P[^/]*)$") + + @defer.inlineCallbacks + def on_GET(self, request, room_alias): + room_alias = self.hs.parse_roomalias(room_alias) + + dir_handler = self.handlers.directory_handler + res = yield dir_handler.get_association(room_alias) + + defer.returnValue((200, res)) + + @defer.inlineCallbacks + def on_PUT(self, request, room_alias): + user = yield self.auth.get_user_by_req(request) + + content = _parse_json(request) + if not "room_id" in content: + raise SynapseError(400, "Missing room_id key", + errcode=Codes.BAD_JSON) + + logger.debug("Got content: %s", content) + + room_alias = self.hs.parse_roomalias(room_alias) + + logger.debug("Got room name: %s", room_alias.to_string()) + + room_id = content["room_id"] + servers = content["servers"] if "servers" in content else None + + logger.debug("Got room_id: %s", room_id) + logger.debug("Got servers: %s", servers) + + # TODO(erikj): Check types. + # TODO(erikj): Check that room exists + + dir_handler = self.handlers.directory_handler + + try: + user_id = user.to_string() + yield dir_handler.create_association( + user_id, room_alias, room_id, servers + ) + yield dir_handler.send_room_alias_update_event(user_id, room_id) + except SynapseError as e: + raise e + except: + logger.exception("Failed to create association") + raise + + defer.returnValue((200, {})) + + @defer.inlineCallbacks + def on_DELETE(self, request, room_alias): + user = yield self.auth.get_user_by_req(request) + + is_admin = yield self.auth.is_server_admin(user) + if not is_admin: + raise AuthError(403, "You need to be a server admin") + + dir_handler = self.handlers.directory_handler + + room_alias = self.hs.parse_roomalias(room_alias) + + yield dir_handler.delete_association( + user.to_string(), room_alias + ) + + defer.returnValue((200, {})) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.", + errcode=Codes.NOT_JSON) + return content + except ValueError: + raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) diff --git a/synapse/client/v1/events.py b/synapse/client/v1/events.py new file mode 100644 index 0000000000..c2515528ac --- /dev/null +++ b/synapse/client/v1/events.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains REST servlets to do with event streaming, /events.""" +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from synapse.streams.config import PaginationConfig +from .base import RestServlet, client_path_pattern + +import logging + + +logger = logging.getLogger(__name__) + + +class EventStreamRestServlet(RestServlet): + PATTERN = client_path_pattern("/events$") + + DEFAULT_LONGPOLL_TIME_MS = 30000 + + @defer.inlineCallbacks + def on_GET(self, request): + auth_user = yield self.auth.get_user_by_req(request) + try: + handler = self.handlers.event_stream_handler + pagin_config = PaginationConfig.from_request(request) + timeout = EventStreamRestServlet.DEFAULT_LONGPOLL_TIME_MS + if "timeout" in request.args: + try: + timeout = int(request.args["timeout"][0]) + except ValueError: + raise SynapseError(400, "timeout must be in milliseconds.") + + as_client_event = "raw" not in request.args + + chunk = yield handler.get_stream( + auth_user.to_string(), pagin_config, timeout=timeout, + as_client_event=as_client_event + ) + except: + logger.exception("Event stream failed") + raise + + defer.returnValue((200, chunk)) + + def on_OPTIONS(self, request): + return (200, {}) + + +# TODO: Unit test gets, with and without auth, with different kinds of events. +class EventRestServlet(RestServlet): + PATTERN = client_path_pattern("/events/(?P[^/]*)$") + + @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, self.hs.serialize_event(event))) + 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/client/v1/initial_sync.py b/synapse/client/v1/initial_sync.py new file mode 100644 index 0000000000..b13d56b286 --- /dev/null +++ b/synapse/client/v1/initial_sync.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from synapse.streams.config import PaginationConfig +from base import RestServlet, client_path_pattern + + +# TODO: Needs unit testing +class InitialSyncRestServlet(RestServlet): + PATTERN = client_path_pattern("/initialSync$") + + @defer.inlineCallbacks + def on_GET(self, request): + user = yield self.auth.get_user_by_req(request) + with_feedback = "feedback" in request.args + as_client_event = "raw" not in request.args + pagination_config = PaginationConfig.from_request(request) + handler = self.handlers.message_handler + content = yield handler.snapshot_all_rooms( + user_id=user.to_string(), + pagin_config=pagination_config, + feedback=with_feedback, + as_client_event=as_client_event + ) + + defer.returnValue((200, content)) + + +def register_servlets(hs, http_server): + InitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/login.py b/synapse/client/v1/login.py new file mode 100644 index 0000000000..6b8deff67b --- /dev/null +++ b/synapse/client/v1/login.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from synapse.types import UserID +from base import RestServlet, client_path_pattern + +import json + + +class LoginRestServlet(RestServlet): + PATTERN = client_path_pattern("/login$") + PASS_TYPE = "m.login.password" + + def on_GET(self, request): + return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]}) + + def on_OPTIONS(self, request): + return (200, {}) + + @defer.inlineCallbacks + def on_POST(self, request): + login_submission = _parse_json(request) + try: + if login_submission["type"] == LoginRestServlet.PASS_TYPE: + result = yield self.do_password_login(login_submission) + defer.returnValue(result) + else: + raise SynapseError(400, "Bad login type.") + except KeyError: + raise SynapseError(400, "Missing JSON keys.") + + @defer.inlineCallbacks + def do_password_login(self, login_submission): + if not login_submission["user"].startswith('@'): + login_submission["user"] = UserID.create( + login_submission["user"], self.hs.hostname).to_string() + + handler = self.handlers.login_handler + token = yield handler.login( + user=login_submission["user"], + password=login_submission["password"]) + + result = { + "user_id": login_submission["user"], # may have changed + "access_token": token, + "home_server": self.hs.hostname, + } + + defer.returnValue((200, result)) + + +class LoginFallbackRestServlet(RestServlet): + PATTERN = client_path_pattern("/login/fallback$") + + def on_GET(self, request): + # TODO(kegan): This should be returning some HTML which is capable of + # hitting LoginRestServlet + return (200, {}) + + +class PasswordResetRestServlet(RestServlet): + PATTERN = client_path_pattern("/login/reset") + + @defer.inlineCallbacks + def on_POST(self, request): + reset_info = _parse_json(request) + try: + email = reset_info["email"] + user_id = reset_info["user_id"] + handler = self.handlers.login_handler + yield handler.reset_password(user_id, email) + # purposefully give no feedback to avoid people hammering different + # combinations. + defer.returnValue((200, {})) + except KeyError: + raise SynapseError( + 400, + "Missing keys. Requires 'email' and 'user_id'." + ) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.") + return content + except ValueError: + raise SynapseError(400, "Content not JSON.") + + +def register_servlets(hs, http_server): + LoginRestServlet(hs).register(http_server) + # TODO PasswordResetRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/presence.py b/synapse/client/v1/presence.py new file mode 100644 index 0000000000..ca4d2d21f0 --- /dev/null +++ b/synapse/client/v1/presence.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with presence: /presence/ +""" +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from base import RestServlet, client_path_pattern + +import json +import logging + +logger = logging.getLogger(__name__) + + +class PresenceStatusRestServlet(RestServlet): + PATTERN = client_path_pattern("/presence/(?P[^/]*)/status") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + state = yield self.handlers.presence_handler.get_state( + target_user=user, auth_user=auth_user) + + defer.returnValue((200, state)) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + state = {} + try: + content = json.loads(request.content.read()) + + state["presence"] = content.pop("presence") + + if "status_msg" in content: + state["status_msg"] = content.pop("status_msg") + if not isinstance(state["status_msg"], basestring): + raise SynapseError(400, "status_msg must be a string.") + + if content: + raise KeyError() + except SynapseError as e: + raise e + except: + raise SynapseError(400, "Unable to parse state") + + yield self.handlers.presence_handler.set_state( + target_user=user, auth_user=auth_user, state=state) + + defer.returnValue((200, {})) + + def on_OPTIONS(self, request): + return (200, {}) + + +class PresenceListRestServlet(RestServlet): + PATTERN = client_path_pattern("/presence/list/(?P[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + if not self.hs.is_mine(user): + raise SynapseError(400, "User not hosted on this Home Server") + + if auth_user != user: + raise SynapseError(400, "Cannot get another user's presence list") + + presence = yield self.handlers.presence_handler.get_presence_list( + observer_user=user, accepted=True) + + for p in presence: + observed_user = p.pop("observed_user") + p["user_id"] = observed_user.to_string() + + defer.returnValue((200, presence)) + + @defer.inlineCallbacks + def on_POST(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + if not self.hs.is_mine(user): + raise SynapseError(400, "User not hosted on this Home Server") + + if auth_user != user: + raise SynapseError( + 400, "Cannot modify another user's presence list") + + try: + content = json.loads(request.content.read()) + except: + logger.exception("JSON parse error") + raise SynapseError(400, "Unable to parse content") + + if "invite" in content: + for u in content["invite"]: + if not isinstance(u, basestring): + raise SynapseError(400, "Bad invite value.") + if len(u) == 0: + continue + invited_user = self.hs.parse_userid(u) + yield self.handlers.presence_handler.send_invite( + observer_user=user, observed_user=invited_user + ) + + if "drop" in content: + for u in content["drop"]: + if not isinstance(u, basestring): + raise SynapseError(400, "Bad drop value.") + if len(u) == 0: + continue + dropped_user = self.hs.parse_userid(u) + yield self.handlers.presence_handler.drop( + observer_user=user, observed_user=dropped_user + ) + + defer.returnValue((200, {})) + + def on_OPTIONS(self, request): + return (200, {}) + + +def register_servlets(hs, http_server): + PresenceStatusRestServlet(hs).register(http_server) + PresenceListRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/profile.py b/synapse/client/v1/profile.py new file mode 100644 index 0000000000..dc6eb424b0 --- /dev/null +++ b/synapse/client/v1/profile.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with profile: /profile/ """ +from twisted.internet import defer + +from base import RestServlet, client_path_pattern + +import json + + +class ProfileDisplaynameRestServlet(RestServlet): + PATTERN = client_path_pattern("/profile/(?P[^/]*)/displayname") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + user = self.hs.parse_userid(user_id) + + displayname = yield self.handlers.profile_handler.get_displayname( + user, + ) + + defer.returnValue((200, {"displayname": displayname})) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + try: + content = json.loads(request.content.read()) + new_name = content["displayname"] + except: + defer.returnValue((400, "Unable to parse name")) + + yield self.handlers.profile_handler.set_displayname( + user, auth_user, new_name) + + defer.returnValue((200, {})) + + def on_OPTIONS(self, request, user_id): + return (200, {}) + + +class ProfileAvatarURLRestServlet(RestServlet): + PATTERN = client_path_pattern("/profile/(?P[^/]*)/avatar_url") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + user = self.hs.parse_userid(user_id) + + avatar_url = yield self.handlers.profile_handler.get_avatar_url( + user, + ) + + defer.returnValue((200, {"avatar_url": avatar_url})) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + try: + content = json.loads(request.content.read()) + new_name = content["avatar_url"] + except: + defer.returnValue((400, "Unable to parse name")) + + yield self.handlers.profile_handler.set_avatar_url( + user, auth_user, new_name) + + defer.returnValue((200, {})) + + def on_OPTIONS(self, request, user_id): + return (200, {}) + + +class ProfileRestServlet(RestServlet): + PATTERN = client_path_pattern("/profile/(?P[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + user = self.hs.parse_userid(user_id) + + displayname = yield self.handlers.profile_handler.get_displayname( + user, + ) + avatar_url = yield self.handlers.profile_handler.get_avatar_url( + user, + ) + + defer.returnValue((200, { + "displayname": displayname, + "avatar_url": avatar_url + })) + + +def register_servlets(hs, http_server): + ProfileDisplaynameRestServlet(hs).register(http_server) + ProfileAvatarURLRestServlet(hs).register(http_server) + ProfileRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/register.py b/synapse/client/v1/register.py new file mode 100644 index 0000000000..e3b26902d9 --- /dev/null +++ b/synapse/client/v1/register.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains REST servlets to do with registration: /register""" +from twisted.internet import defer + +from synapse.api.errors import SynapseError, Codes +from synapse.api.constants import LoginType +from base import RestServlet, client_path_pattern +import synapse.util.stringutils as stringutils + +from synapse.util.async import run_on_reactor + +from hashlib import sha1 +import hmac +import json +import logging +import urllib + +logger = logging.getLogger(__name__) + + +# We ought to be using hmac.compare_digest() but on older pythons it doesn't +# exist. It's a _really minor_ security flaw to use plain string comparison +# because the timing attack is so obscured by all the other code here it's +# unlikely to make much difference +if hasattr(hmac, "compare_digest"): + compare_digest = hmac.compare_digest +else: + compare_digest = lambda a, b: a == b + + +class RegisterRestServlet(RestServlet): + """Handles registration with the home server. + + This servlet is in control of the registration flow; the registration + handler doesn't have a concept of multi-stages or sessions. + """ + + PATTERN = client_path_pattern("/register$") + + def __init__(self, hs): + super(RegisterRestServlet, self).__init__(hs) + # sessions are stored as: + # self.sessions = { + # "session_id" : { __session_dict__ } + # } + # TODO: persistent storage + self.sessions = {} + + def on_GET(self, request): + if self.hs.config.enable_registration_captcha: + return ( + 200, + {"flows": [ + { + "type": LoginType.RECAPTCHA, + "stages": [ + LoginType.RECAPTCHA, + LoginType.EMAIL_IDENTITY, + LoginType.PASSWORD + ] + }, + { + "type": LoginType.RECAPTCHA, + "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] + } + ]} + ) + else: + return ( + 200, + {"flows": [ + { + "type": LoginType.EMAIL_IDENTITY, + "stages": [ + LoginType.EMAIL_IDENTITY, LoginType.PASSWORD + ] + }, + { + "type": LoginType.PASSWORD + } + ]} + ) + + @defer.inlineCallbacks + def on_POST(self, request): + register_json = _parse_json(request) + + session = (register_json["session"] + if "session" in register_json else None) + login_type = None + if "type" not in register_json: + raise SynapseError(400, "Missing 'type' key.") + + try: + login_type = register_json["type"] + stages = { + LoginType.RECAPTCHA: self._do_recaptcha, + LoginType.PASSWORD: self._do_password, + LoginType.EMAIL_IDENTITY: self._do_email_identity + } + + session_info = self._get_session_info(request, session) + logger.debug("%s : session info %s request info %s", + login_type, session_info, register_json) + response = yield stages[login_type]( + request, + register_json, + session_info + ) + + if "access_token" not in response: + # isn't a final response + response["session"] = session_info["id"] + + defer.returnValue((200, response)) + except KeyError as e: + logger.exception(e) + raise SynapseError(400, "Missing JSON keys for login type %s." % ( + login_type, + )) + + def on_OPTIONS(self, request): + return (200, {}) + + def _get_session_info(self, request, session_id): + if not session_id: + # create a new session + while session_id is None or session_id in self.sessions: + session_id = stringutils.random_string(24) + self.sessions[session_id] = { + "id": session_id, + LoginType.EMAIL_IDENTITY: False, + LoginType.RECAPTCHA: False + } + + return self.sessions[session_id] + + def _save_session(self, session): + # TODO: Persistent storage + logger.debug("Saving session %s", session) + self.sessions[session["id"]] = session + + def _remove_session(self, session): + logger.debug("Removing session %s", session) + self.sessions.pop(session["id"]) + + @defer.inlineCallbacks + def _do_recaptcha(self, request, register_json, session): + if not self.hs.config.enable_registration_captcha: + raise SynapseError(400, "Captcha not required.") + + yield self._check_recaptcha(request, register_json, session) + + session[LoginType.RECAPTCHA] = True # mark captcha as done + self._save_session(session) + defer.returnValue({ + "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY] + }) + + @defer.inlineCallbacks + def _check_recaptcha(self, request, register_json, session): + if ("captcha_bypass_hmac" in register_json and + self.hs.config.captcha_bypass_secret): + if "user" not in register_json: + raise SynapseError(400, "Captcha bypass needs 'user'") + + want = hmac.new( + key=self.hs.config.captcha_bypass_secret, + msg=register_json["user"], + digestmod=sha1, + ).hexdigest() + + # str() because otherwise hmac complains that 'unicode' does not + # have the buffer interface + got = str(register_json["captcha_bypass_hmac"]) + + if compare_digest(want, got): + session["user"] = register_json["user"] + defer.returnValue(None) + else: + raise SynapseError( + 400, "Captcha bypass HMAC incorrect", + errcode=Codes.CAPTCHA_NEEDED + ) + + challenge = None + user_response = None + try: + challenge = register_json["challenge"] + user_response = register_json["response"] + except KeyError: + raise SynapseError(400, "Captcha response is required", + errcode=Codes.CAPTCHA_NEEDED) + + ip_addr = self.hs.get_ip_from_request(request) + + handler = self.handlers.registration_handler + yield handler.check_recaptcha( + ip_addr, + self.hs.config.recaptcha_private_key, + challenge, + user_response + ) + + @defer.inlineCallbacks + def _do_email_identity(self, request, register_json, session): + if (self.hs.config.enable_registration_captcha and + not session[LoginType.RECAPTCHA]): + raise SynapseError(400, "Captcha is required.") + + threepidCreds = register_json['threepidCreds'] + handler = self.handlers.registration_handler + logger.debug("Registering email. threepidcreds: %s" % (threepidCreds)) + yield handler.register_email(threepidCreds) + session["threepidCreds"] = threepidCreds # store creds for next stage + session[LoginType.EMAIL_IDENTITY] = True # mark email as done + self._save_session(session) + defer.returnValue({ + "next": LoginType.PASSWORD + }) + + @defer.inlineCallbacks + def _do_password(self, request, register_json, session): + yield run_on_reactor() + if (self.hs.config.enable_registration_captcha and + not session[LoginType.RECAPTCHA]): + # captcha should've been done by this stage! + raise SynapseError(400, "Captcha is required.") + + if ("user" in session and "user" in register_json and + session["user"] != register_json["user"]): + raise SynapseError( + 400, "Cannot change user ID during registration" + ) + + password = register_json["password"].encode("utf-8") + desired_user_id = (register_json["user"].encode("utf-8") + if "user" in register_json else None) + if (desired_user_id + and urllib.quote(desired_user_id) != desired_user_id): + raise SynapseError( + 400, + "User ID must only contain characters which do not " + + "require URL encoding.") + handler = self.handlers.registration_handler + (user_id, token) = yield handler.register( + localpart=desired_user_id, + password=password + ) + + if session[LoginType.EMAIL_IDENTITY]: + logger.debug("Binding emails %s to %s" % ( + session["threepidCreds"], user_id) + ) + yield handler.bind_emails(user_id, session["threepidCreds"]) + + result = { + "user_id": user_id, + "access_token": token, + "home_server": self.hs.hostname, + } + self._remove_session(session) + defer.returnValue(result) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.") + return content + except ValueError: + raise SynapseError(400, "Content not JSON.") + + +def register_servlets(hs, http_server): + RegisterRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/room.py b/synapse/client/v1/room.py new file mode 100644 index 0000000000..48bba2a5f3 --- /dev/null +++ b/synapse/client/v1/room.py @@ -0,0 +1,559 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with rooms: /rooms/ """ +from twisted.internet import defer + +from base import RestServlet, client_path_pattern +from synapse.api.errors import SynapseError, Codes +from synapse.streams.config import PaginationConfig +from synapse.api.constants import EventTypes, Membership + +import json +import logging +import urllib + + +logger = logging.getLogger(__name__) + + +class RoomCreateRestServlet(RestServlet): + # No PATTERN; we have custom dispatch rules here + + def register(self, http_server): + 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, txn_id): + try: + defer.returnValue( + self.txns.get_client_transaction(request, txn_id) + ) + except KeyError: + pass + + response = yield self.on_POST(request) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + + @defer.inlineCallbacks + def on_POST(self, request): + auth_user = yield self.auth.get_user_by_req(request) + + room_config = self.get_room_config(request) + info = yield self.make_room(room_config, auth_user, None) + room_config.update(info) + defer.returnValue((200, info)) + + @defer.inlineCallbacks + def make_room(self, room_config, auth_user, room_id): + handler = self.handlers.room_creation_handler + info = yield handler.create_room( + user_id=auth_user.to_string(), + room_id=room_id, + config=room_config + ) + defer.returnValue(info) + + def get_room_config(self, request): + try: + user_supplied_config = json.loads(request.content.read()) + if "visibility" not in user_supplied_config: + # default visibility + user_supplied_config["visibility"] = "public" + return user_supplied_config + except (ValueError, TypeError): + raise SynapseError(400, "Body must be JSON.", + errcode=Codes.BAD_JSON) + + def on_OPTIONS(self, request): + return (200, {}) + + +# TODO: Needs unit testing for generic events +class RoomStateEventRestServlet(RestServlet): + def register(self, http_server): + # /room/$roomid/state/$eventtype + no_state_key = "/rooms/(?P[^/]*)/state/(?P[^/]*)$" + + # /room/$roomid/state/$eventtype/$statekey + state_key = ("/rooms/(?P[^/]*)/state/" + "(?P[^/]*)/(?P[^/]*)$") + + http_server.register_path("GET", + client_path_pattern(state_key), + self.on_GET) + http_server.register_path("PUT", + client_path_pattern(state_key), + self.on_PUT) + http_server.register_path("GET", + client_path_pattern(no_state_key), + self.on_GET_no_state_key) + http_server.register_path("PUT", + client_path_pattern(no_state_key), + self.on_PUT_no_state_key) + + def on_GET_no_state_key(self, request, room_id, event_type): + return self.on_GET(request, room_id, event_type, "") + + def on_PUT_no_state_key(self, request, room_id, event_type): + return self.on_PUT(request, room_id, event_type, "") + + @defer.inlineCallbacks + def on_GET(self, request, room_id, event_type, state_key): + user = yield self.auth.get_user_by_req(request) + + msg_handler = self.handlers.message_handler + data = yield msg_handler.get_room_data( + user_id=user.to_string(), + room_id=room_id, + event_type=event_type, + state_key=state_key, + ) + + if not data: + raise SynapseError( + 404, "Event not found.", errcode=Codes.NOT_FOUND + ) + defer.returnValue((200, data.get_dict()["content"])) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, event_type, state_key): + user = yield self.auth.get_user_by_req(request) + + content = _parse_json(request) + + event_dict = { + "type": event_type, + "content": content, + "room_id": room_id, + "sender": user.to_string(), + } + + if state_key is not None: + event_dict["state_key"] = state_key + + msg_handler = self.handlers.message_handler + yield msg_handler.create_and_send_event(event_dict) + + defer.returnValue((200, {})) + + +# TODO: Needs unit testing for generic events + feedback +class RoomSendEventRestServlet(RestServlet): + + def register(self, http_server): + # /rooms/$roomid/send/$event_type[/$txn_id] + PATTERN = ("/rooms/(?P[^/]*)/send/(?P[^/]*)") + register_txn_path(self, PATTERN, http_server, with_get=True) + + @defer.inlineCallbacks + def on_POST(self, request, room_id, event_type): + user = yield self.auth.get_user_by_req(request) + content = _parse_json(request) + + msg_handler = self.handlers.message_handler + event = yield msg_handler.create_and_send_event( + { + "type": event_type, + "content": content, + "room_id": room_id, + "sender": user.to_string(), + } + ) + + defer.returnValue((200, {"event_id": event.event_id})) + + def on_GET(self, request, room_id, event_type, txn_id): + return (200, "Not implemented") + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, event_type, txn_id): + try: + defer.returnValue( + self.txns.get_client_transaction(request, txn_id) + ) + except KeyError: + pass + + response = yield self.on_POST(request, room_id, event_type) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + + +# TODO: Needs unit testing for room ID + alias joins +class JoinRoomAliasServlet(RestServlet): + + def register(self, http_server): + # /join/$room_identifier[/$txn_id] + PATTERN = ("/join/(?P[^/]*)") + register_txn_path(self, PATTERN, http_server) + + @defer.inlineCallbacks + def on_POST(self, request, room_identifier): + user = yield self.auth.get_user_by_req(request) + + # 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. + + identifier = None + is_room_alias = False + try: + identifier = self.hs.parse_roomalias(room_identifier) + is_room_alias = True + except SynapseError: + identifier = self.hs.parse_roomid(room_identifier) + + # TODO: Support for specifying the home server to join with? + + 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 + msg_handler = self.handlers.message_handler + yield msg_handler.create_and_send_event( + { + "type": EventTypes.Member, + "content": {"membership": Membership.JOIN}, + "room_id": identifier.to_string(), + "sender": user.to_string(), + "state_key": user.to_string(), + } + ) + + defer.returnValue((200, {"room_id": identifier.to_string()})) + + @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 +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[^/]*)/members$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + # TODO support Pagination stream API (limit/tokens) + user = yield self.auth.get_user_by_req(request) + handler = self.handlers.room_member_handler + members = yield handler.get_room_members_as_pagination_chunk( + room_id=room_id, + user_id=user.to_string()) + + for event in members["chunk"]: + # FIXME: should probably be state_key here, not user_id + target_user = self.hs.parse_userid(event["user_id"]) + # Presence is an optional cache; don't fail if we can't fetch it + try: + presence_handler = self.handlers.presence_handler + presence_state = yield presence_handler.get_state( + target_user=target_user, auth_user=user + ) + event["content"].update(presence_state) + except: + pass + + defer.returnValue((200, members)) + + +# TODO: Needs unit testing +class RoomMessageListRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P[^/]*)/messages$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + pagination_config = PaginationConfig.from_request( + request, default_limit=10, + ) + with_feedback = "feedback" in request.args + as_client_event = "raw" not in request.args + handler = self.handlers.message_handler + msgs = yield handler.get_messages( + room_id=room_id, + user_id=user.to_string(), + pagin_config=pagination_config, + feedback=with_feedback, + as_client_event=as_client_event + ) + + defer.returnValue((200, msgs)) + + +# TODO: Needs unit testing +class RoomStateRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P[^/]*)/state$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + handler = self.handlers.message_handler + # Get all the current state for this room + events = yield handler.get_state_events( + room_id=room_id, + user_id=user.to_string(), + ) + defer.returnValue((200, events)) + + +# TODO: Needs unit testing +class RoomInitialSyncRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P[^/]*)/initialSync$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + pagination_config = PaginationConfig.from_request(request) + content = yield self.handlers.message_handler.room_initial_sync( + room_id=room_id, + user_id=user.to_string(), + pagin_config=pagination_config, + ) + defer.returnValue((200, content)) + + +class RoomTriggerBackfill(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P[^/]*)/backfill$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + remote_server = urllib.unquote( + request.args["remote"][0] + ).decode("UTF-8") + + limit = int(request.args["limit"][0]) + + handler = self.handlers.federation_handler + events = yield handler.backfill(remote_server, room_id, limit) + + res = [self.hs.serialize_event(event) for event in events] + defer.returnValue((200, res)) + + +# TODO: Needs unit testing +class RoomMembershipRestServlet(RestServlet): + + def register(self, http_server): + # /rooms/$roomid/[invite|join|leave] + PATTERN = ("/rooms/(?P[^/]*)/" + "(?Pjoin|invite|leave|ban|kick)") + register_txn_path(self, PATTERN, http_server) + + @defer.inlineCallbacks + def on_POST(self, request, room_id, membership_action): + user = yield self.auth.get_user_by_req(request) + + content = _parse_json(request) + + # target user is you unless it is an invite + state_key = user.to_string() + if membership_action in ["invite", "ban", "kick"]: + if "user_id" not in content: + raise SynapseError(400, "Missing user_id key.") + state_key = content["user_id"] + + if membership_action == "kick": + membership_action = "leave" + + msg_handler = self.handlers.message_handler + yield msg_handler.create_and_send_event( + { + "type": EventTypes.Member, + "content": {"membership": unicode(membership_action)}, + "room_id": room_id, + "sender": user.to_string(), + "state_key": state_key, + } + ) + + defer.returnValue((200, {})) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, membership_action, txn_id): + try: + defer.returnValue( + self.txns.get_client_transaction(request, txn_id) + ) + except KeyError: + pass + + response = yield self.on_POST(request, room_id, membership_action) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + + +class RoomRedactEventRestServlet(RestServlet): + def register(self, http_server): + PATTERN = ("/rooms/(?P[^/]*)/redact/(?P[^/]*)") + register_txn_path(self, PATTERN, http_server) + + @defer.inlineCallbacks + def on_POST(self, request, room_id, event_id): + user = yield self.auth.get_user_by_req(request) + content = _parse_json(request) + + msg_handler = self.handlers.message_handler + event = yield msg_handler.create_and_send_event( + { + "type": EventTypes.Redaction, + "content": content, + "room_id": room_id, + "sender": user.to_string(), + "redacts": event_id, + } + ) + + defer.returnValue((200, {"event_id": event.event_id})) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, event_id, txn_id): + try: + defer.returnValue( + self.txns.get_client_transaction(request, txn_id) + ) + except KeyError: + pass + + response = yield self.on_POST(request, room_id, event_id) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + + +class RoomTypingRestServlet(RestServlet): + PATTERN = client_path_pattern( + "/rooms/(?P[^/]*)/typing/(?P[^/]*)$" + ) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, user_id): + auth_user = yield self.auth.get_user_by_req(request) + + room_id = urllib.unquote(room_id) + target_user = self.hs.parse_userid(urllib.unquote(user_id)) + + content = _parse_json(request) + + typing_handler = self.handlers.typing_notification_handler + + if content["typing"]: + yield typing_handler.started_typing( + target_user=target_user, + auth_user=auth_user, + room_id=room_id, + timeout=content.get("timeout", 30000), + ) + else: + yield typing_handler.stopped_typing( + target_user=target_user, + auth_user=auth_user, + room_id=room_id, + ) + + defer.returnValue((200, {})) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.", + errcode=Codes.NOT_JSON) + return content + except ValueError: + raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) + + +def register_txn_path(servlet, regex_string, http_server, with_get=False): + """Registers a transaction-based path. + + This registers two paths: + PUT regex_string/$txnid + POST regex_string + + Args: + regex_string (str): The regex string to register. Must NOT have a + trailing $ as this string will be appended to. + http_server : The http_server to register paths with. + with_get: True to also register respective GET paths for the PUTs. + """ + http_server.register_path( + "POST", + client_path_pattern(regex_string + "$"), + servlet.on_POST + ) + http_server.register_path( + "PUT", + client_path_pattern(regex_string + "/(?P[^/]*)$"), + servlet.on_PUT + ) + if with_get: + http_server.register_path( + "GET", + client_path_pattern(regex_string + "/(?P[^/]*)$"), + servlet.on_GET + ) + + +def register_servlets(hs, http_server): + RoomStateEventRestServlet(hs).register(http_server) + RoomCreateRestServlet(hs).register(http_server) + RoomMemberListRestServlet(hs).register(http_server) + RoomMessageListRestServlet(hs).register(http_server) + JoinRoomAliasServlet(hs).register(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) + RoomRedactEventRestServlet(hs).register(http_server) + RoomTypingRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/transactions.py b/synapse/client/v1/transactions.py new file mode 100644 index 0000000000..d933fea18a --- /dev/null +++ b/synapse/client/v1/transactions.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains logic for storing HTTP PUT transactions. This is used +to ensure idempotency when performing PUTs using the REST API.""" +import logging + +logger = logging.getLogger(__name__) + + +# FIXME: elsewhere we use FooStore to indicate something in the storage layer... +class HttpTransactionStore(object): + + def __init__(self): + # { key : (txn_id, response) } + self.transactions = {} + + def get_response(self, key, txn_id): + """Retrieve a response for this request. + + Args: + key (str): A transaction-independent key for this request. Usually + this is a combination of the path (without the transaction id) + and the user's access token. + txn_id (str): The transaction ID for this request + Returns: + A tuple of (HTTP response code, response content) or None. + """ + try: + logger.debug("get_response Key: %s TxnId: %s", key, txn_id) + (last_txn_id, response) = self.transactions[key] + if txn_id == last_txn_id: + logger.info("get_response: Returning a response for %s", key) + return response + except KeyError: + pass + return None + + def store_response(self, key, txn_id, response): + """Stores an HTTP response tuple. + + Args: + key (str): A transaction-independent key for this request. Usually + this is a combination of the path (without the transaction id) + and the user's access token. + txn_id (str): The transaction ID for this request. + response (tuple): A tuple of (HTTP response code, response content) + """ + logger.debug("store_response Key: %s TxnId: %s", key, txn_id) + self.transactions[key] = (txn_id, response) + + def store_client_transaction(self, request, txn_id, response): + """Stores the request/response pair of an HTTP transaction. + + Args: + request (twisted.web.http.Request): The twisted HTTP request. This + request must have the transaction ID as the last path segment. + response (tuple): A tuple of (response code, response dict) + txn_id (str): The transaction ID for this request. + """ + self.store_response(self._get_key(request), txn_id, response) + + def get_client_transaction(self, request, txn_id): + """Retrieves a stored response if there was one. + + Args: + request (twisted.web.http.Request): The twisted HTTP request. This + request must have the transaction ID as the last path segment. + txn_id (str): The transaction ID for this request. + Returns: + The response tuple. + Raises: + KeyError if the transaction was not found. + """ + response = self.get_response(self._get_key(request), txn_id) + if response is None: + raise KeyError("Transaction not found.") + return response + + def _get_key(self, request): + token = request.args["access_token"][0] + path_without_txn_id = request.path.rsplit("/", 1)[0] + return path_without_txn_id + "/" + token diff --git a/synapse/client/v1/voip.py b/synapse/client/v1/voip.py new file mode 100644 index 0000000000..011c35e69b --- /dev/null +++ b/synapse/client/v1/voip.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from base import RestServlet, client_path_pattern + + +import hmac +import hashlib +import base64 + + +class VoipRestServlet(RestServlet): + PATTERN = client_path_pattern("/voip/turnServer$") + + @defer.inlineCallbacks + def on_GET(self, request): + auth_user = yield self.auth.get_user_by_req(request) + + turnUris = self.hs.config.turn_uris + turnSecret = self.hs.config.turn_shared_secret + userLifetime = self.hs.config.turn_user_lifetime + if not turnUris or not turnSecret or not userLifetime: + defer.returnValue((200, {})) + + expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000 + username = "%d:%s" % (expiry, auth_user.to_string()) + + mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1) + # We need to use standard base64 encoding here, *not* syutil's + # encode_base64 because we need to add the standard padding to get the + # same result as the TURN server. + password = base64.b64encode(mac.digest()) + + defer.returnValue((200, { + 'username': username, + 'password': password, + 'ttl': userLifetime / 1000, + 'uris': turnUris, + })) + + def on_OPTIONS(self, request): + return (200, {}) + + +def register_servlets(hs, http_server): + VoipRestServlet(hs).register(http_server) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py deleted file mode 100644 index 88ec9cd27d..0000000000 --- a/synapse/rest/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from . import ( - room, events, register, login, profile, presence, initial_sync, directory, - voip, admin, -) - - -class RestServletFactory(object): - - """ A factory for creating REST servlets. - - These REST servlets represent the entire client-server REST API. Generally - speaking, they serve as wrappers around events and the handlers that - process them. - - See synapse.events for information on synapse events. - """ - - def __init__(self, hs): - client_resource = hs.get_resource_for_client() - - # TODO(erikj): There *must* be a better way of doing this. - room.register_servlets(hs, client_resource) - events.register_servlets(hs, client_resource) - register.register_servlets(hs, client_resource) - login.register_servlets(hs, client_resource) - profile.register_servlets(hs, client_resource) - presence.register_servlets(hs, client_resource) - initial_sync.register_servlets(hs, client_resource) - directory.register_servlets(hs, client_resource) - voip.register_servlets(hs, client_resource) - admin.register_servlets(hs, client_resource) diff --git a/synapse/rest/admin.py b/synapse/rest/admin.py deleted file mode 100644 index 0aa83514c8..0000000000 --- a/synapse/rest/admin.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.internet import defer - -from synapse.api.errors import AuthError, SynapseError -from base import RestServlet, client_path_pattern - -import logging - -logger = logging.getLogger(__name__) - - -class WhoisRestServlet(RestServlet): - PATTERN = client_path_pattern("/admin/whois/(?P[^/]*)") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - target_user = self.hs.parse_userid(user_id) - auth_user = yield self.auth.get_user_by_req(request) - is_admin = yield self.auth.is_server_admin(auth_user) - - if not is_admin and target_user != auth_user: - raise AuthError(403, "You are not a server admin") - - if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only whois a local user") - - ret = yield self.handlers.admin_handler.get_whois(target_user) - - defer.returnValue((200, ret)) - - -def register_servlets(hs, http_server): - WhoisRestServlet(hs).register(http_server) diff --git a/synapse/rest/base.py b/synapse/rest/base.py deleted file mode 100644 index c583945527..0000000000 --- a/synapse/rest/base.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains base REST classes for constructing REST servlets. """ -from synapse.api.urls import CLIENT_PREFIX -from synapse.rest.transactions import HttpTransactionStore -import re - -import logging - - -logger = logging.getLogger(__name__) - - -def client_path_pattern(path_regex): - """Creates a regex compiled client path with the correct client path - prefix. - - Args: - path_regex (str): The regex string to match. This should NOT have a ^ - as this will be prefixed. - Returns: - SRE_Pattern - """ - return re.compile("^" + CLIENT_PREFIX + path_regex) - - -class RestServlet(object): - - """ A Synapse REST Servlet. - - An implementing class can either provide its own custom 'register' method, - or use the automatic pattern handling provided by the base class. - - To use this latter, the implementing class instead provides a `PATTERN` - class attribute containing a pre-compiled regular expression. The automatic - register method will then use this method to register any of the following - instance methods associated with the corresponding HTTP method: - - on_GET - on_PUT - on_POST - on_DELETE - on_OPTIONS - - Automatically handles turning CodeMessageExceptions thrown by these methods - into the appropriate HTTP response. - """ - - def __init__(self, hs): - self.hs = hs - - self.handlers = hs.get_handlers() - self.builder_factory = hs.get_event_builder_factory() - self.auth = hs.get_auth() - self.txns = HttpTransactionStore() - - def register(self, http_server): - """ Register this servlet with the given HTTP server. """ - if hasattr(self, "PATTERN"): - pattern = self.PATTERN - - for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"): - if hasattr(self, "on_%s" % (method)): - method_handler = getattr(self, "on_%s" % (method)) - http_server.register_path(method, pattern, method_handler) - else: - raise NotImplementedError("RestServlet must register something.") diff --git a/synapse/rest/directory.py b/synapse/rest/directory.py deleted file mode 100644 index 7ff44fdd9e..0000000000 --- a/synapse/rest/directory.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from twisted.internet import defer - -from synapse.api.errors import AuthError, SynapseError, Codes -from base import RestServlet, client_path_pattern - -import json -import logging - - -logger = logging.getLogger(__name__) - - -def register_servlets(hs, http_server): - ClientDirectoryServer(hs).register(http_server) - - -class ClientDirectoryServer(RestServlet): - PATTERN = client_path_pattern("/directory/room/(?P[^/]*)$") - - @defer.inlineCallbacks - def on_GET(self, request, room_alias): - room_alias = self.hs.parse_roomalias(room_alias) - - dir_handler = self.handlers.directory_handler - res = yield dir_handler.get_association(room_alias) - - defer.returnValue((200, res)) - - @defer.inlineCallbacks - def on_PUT(self, request, room_alias): - user = yield self.auth.get_user_by_req(request) - - content = _parse_json(request) - if not "room_id" in content: - raise SynapseError(400, "Missing room_id key", - errcode=Codes.BAD_JSON) - - logger.debug("Got content: %s", content) - - room_alias = self.hs.parse_roomalias(room_alias) - - logger.debug("Got room name: %s", room_alias.to_string()) - - room_id = content["room_id"] - servers = content["servers"] if "servers" in content else None - - logger.debug("Got room_id: %s", room_id) - logger.debug("Got servers: %s", servers) - - # TODO(erikj): Check types. - # TODO(erikj): Check that room exists - - dir_handler = self.handlers.directory_handler - - try: - user_id = user.to_string() - yield dir_handler.create_association( - user_id, room_alias, room_id, servers - ) - yield dir_handler.send_room_alias_update_event(user_id, room_id) - except SynapseError as e: - raise e - except: - logger.exception("Failed to create association") - raise - - defer.returnValue((200, {})) - - @defer.inlineCallbacks - def on_DELETE(self, request, room_alias): - user = yield self.auth.get_user_by_req(request) - - is_admin = yield self.auth.is_server_admin(user) - if not is_admin: - raise AuthError(403, "You need to be a server admin") - - dir_handler = self.handlers.directory_handler - - room_alias = self.hs.parse_roomalias(room_alias) - - yield dir_handler.delete_association( - user.to_string(), room_alias - ) - - defer.returnValue((200, {})) - - -def _parse_json(request): - try: - content = json.loads(request.content.read()) - if type(content) != dict: - raise SynapseError(400, "Content must be a JSON object.", - errcode=Codes.NOT_JSON) - return content - except ValueError: - raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) diff --git a/synapse/rest/events.py b/synapse/rest/events.py deleted file mode 100644 index bedcb2bcc6..0000000000 --- a/synapse/rest/events.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains REST servlets to do with event streaming, /events.""" -from twisted.internet import defer - -from synapse.api.errors import SynapseError -from synapse.streams.config import PaginationConfig -from synapse.rest.base import RestServlet, client_path_pattern - -import logging - - -logger = logging.getLogger(__name__) - - -class EventStreamRestServlet(RestServlet): - PATTERN = client_path_pattern("/events$") - - DEFAULT_LONGPOLL_TIME_MS = 30000 - - @defer.inlineCallbacks - def on_GET(self, request): - auth_user = yield self.auth.get_user_by_req(request) - try: - handler = self.handlers.event_stream_handler - pagin_config = PaginationConfig.from_request(request) - timeout = EventStreamRestServlet.DEFAULT_LONGPOLL_TIME_MS - if "timeout" in request.args: - try: - timeout = int(request.args["timeout"][0]) - except ValueError: - raise SynapseError(400, "timeout must be in milliseconds.") - - as_client_event = "raw" not in request.args - - chunk = yield handler.get_stream( - auth_user.to_string(), pagin_config, timeout=timeout, - as_client_event=as_client_event - ) - except: - logger.exception("Event stream failed") - raise - - defer.returnValue((200, chunk)) - - def on_OPTIONS(self, request): - return (200, {}) - - -# TODO: Unit test gets, with and without auth, with different kinds of events. -class EventRestServlet(RestServlet): - PATTERN = client_path_pattern("/events/(?P[^/]*)$") - - @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, self.hs.serialize_event(event))) - 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/initial_sync.py b/synapse/rest/initial_sync.py deleted file mode 100644 index b13d56b286..0000000000 --- a/synapse/rest/initial_sync.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.internet import defer - -from synapse.streams.config import PaginationConfig -from base import RestServlet, client_path_pattern - - -# TODO: Needs unit testing -class InitialSyncRestServlet(RestServlet): - PATTERN = client_path_pattern("/initialSync$") - - @defer.inlineCallbacks - def on_GET(self, request): - user = yield self.auth.get_user_by_req(request) - with_feedback = "feedback" in request.args - as_client_event = "raw" not in request.args - pagination_config = PaginationConfig.from_request(request) - handler = self.handlers.message_handler - content = yield handler.snapshot_all_rooms( - user_id=user.to_string(), - pagin_config=pagination_config, - feedback=with_feedback, - as_client_event=as_client_event - ) - - defer.returnValue((200, content)) - - -def register_servlets(hs, http_server): - InitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/login.py b/synapse/rest/login.py deleted file mode 100644 index 6b8deff67b..0000000000 --- a/synapse/rest/login.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.internet import defer - -from synapse.api.errors import SynapseError -from synapse.types import UserID -from base import RestServlet, client_path_pattern - -import json - - -class LoginRestServlet(RestServlet): - PATTERN = client_path_pattern("/login$") - PASS_TYPE = "m.login.password" - - def on_GET(self, request): - return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]}) - - def on_OPTIONS(self, request): - return (200, {}) - - @defer.inlineCallbacks - def on_POST(self, request): - login_submission = _parse_json(request) - try: - if login_submission["type"] == LoginRestServlet.PASS_TYPE: - result = yield self.do_password_login(login_submission) - defer.returnValue(result) - else: - raise SynapseError(400, "Bad login type.") - except KeyError: - raise SynapseError(400, "Missing JSON keys.") - - @defer.inlineCallbacks - def do_password_login(self, login_submission): - if not login_submission["user"].startswith('@'): - login_submission["user"] = UserID.create( - login_submission["user"], self.hs.hostname).to_string() - - handler = self.handlers.login_handler - token = yield handler.login( - user=login_submission["user"], - password=login_submission["password"]) - - result = { - "user_id": login_submission["user"], # may have changed - "access_token": token, - "home_server": self.hs.hostname, - } - - defer.returnValue((200, result)) - - -class LoginFallbackRestServlet(RestServlet): - PATTERN = client_path_pattern("/login/fallback$") - - def on_GET(self, request): - # TODO(kegan): This should be returning some HTML which is capable of - # hitting LoginRestServlet - return (200, {}) - - -class PasswordResetRestServlet(RestServlet): - PATTERN = client_path_pattern("/login/reset") - - @defer.inlineCallbacks - def on_POST(self, request): - reset_info = _parse_json(request) - try: - email = reset_info["email"] - user_id = reset_info["user_id"] - handler = self.handlers.login_handler - yield handler.reset_password(user_id, email) - # purposefully give no feedback to avoid people hammering different - # combinations. - defer.returnValue((200, {})) - except KeyError: - raise SynapseError( - 400, - "Missing keys. Requires 'email' and 'user_id'." - ) - - -def _parse_json(request): - try: - content = json.loads(request.content.read()) - if type(content) != dict: - raise SynapseError(400, "Content must be a JSON object.") - return content - except ValueError: - raise SynapseError(400, "Content not JSON.") - - -def register_servlets(hs, http_server): - LoginRestServlet(hs).register(http_server) - # TODO PasswordResetRestServlet(hs).register(http_server) diff --git a/synapse/rest/presence.py b/synapse/rest/presence.py deleted file mode 100644 index ca4d2d21f0..0000000000 --- a/synapse/rest/presence.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with presence: /presence/ -""" -from twisted.internet import defer - -from synapse.api.errors import SynapseError -from base import RestServlet, client_path_pattern - -import json -import logging - -logger = logging.getLogger(__name__) - - -class PresenceStatusRestServlet(RestServlet): - PATTERN = client_path_pattern("/presence/(?P[^/]*)/status") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - state = yield self.handlers.presence_handler.get_state( - target_user=user, auth_user=auth_user) - - defer.returnValue((200, state)) - - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - state = {} - try: - content = json.loads(request.content.read()) - - state["presence"] = content.pop("presence") - - if "status_msg" in content: - state["status_msg"] = content.pop("status_msg") - if not isinstance(state["status_msg"], basestring): - raise SynapseError(400, "status_msg must be a string.") - - if content: - raise KeyError() - except SynapseError as e: - raise e - except: - raise SynapseError(400, "Unable to parse state") - - yield self.handlers.presence_handler.set_state( - target_user=user, auth_user=auth_user, state=state) - - defer.returnValue((200, {})) - - def on_OPTIONS(self, request): - return (200, {}) - - -class PresenceListRestServlet(RestServlet): - PATTERN = client_path_pattern("/presence/list/(?P[^/]*)") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - if not self.hs.is_mine(user): - raise SynapseError(400, "User not hosted on this Home Server") - - if auth_user != user: - raise SynapseError(400, "Cannot get another user's presence list") - - presence = yield self.handlers.presence_handler.get_presence_list( - observer_user=user, accepted=True) - - for p in presence: - observed_user = p.pop("observed_user") - p["user_id"] = observed_user.to_string() - - defer.returnValue((200, presence)) - - @defer.inlineCallbacks - def on_POST(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - if not self.hs.is_mine(user): - raise SynapseError(400, "User not hosted on this Home Server") - - if auth_user != user: - raise SynapseError( - 400, "Cannot modify another user's presence list") - - try: - content = json.loads(request.content.read()) - except: - logger.exception("JSON parse error") - raise SynapseError(400, "Unable to parse content") - - if "invite" in content: - for u in content["invite"]: - if not isinstance(u, basestring): - raise SynapseError(400, "Bad invite value.") - if len(u) == 0: - continue - invited_user = self.hs.parse_userid(u) - yield self.handlers.presence_handler.send_invite( - observer_user=user, observed_user=invited_user - ) - - if "drop" in content: - for u in content["drop"]: - if not isinstance(u, basestring): - raise SynapseError(400, "Bad drop value.") - if len(u) == 0: - continue - dropped_user = self.hs.parse_userid(u) - yield self.handlers.presence_handler.drop( - observer_user=user, observed_user=dropped_user - ) - - defer.returnValue((200, {})) - - def on_OPTIONS(self, request): - return (200, {}) - - -def register_servlets(hs, http_server): - PresenceStatusRestServlet(hs).register(http_server) - PresenceListRestServlet(hs).register(http_server) diff --git a/synapse/rest/profile.py b/synapse/rest/profile.py deleted file mode 100644 index dc6eb424b0..0000000000 --- a/synapse/rest/profile.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with profile: /profile/ """ -from twisted.internet import defer - -from base import RestServlet, client_path_pattern - -import json - - -class ProfileDisplaynameRestServlet(RestServlet): - PATTERN = client_path_pattern("/profile/(?P[^/]*)/displayname") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) - - displayname = yield self.handlers.profile_handler.get_displayname( - user, - ) - - defer.returnValue((200, {"displayname": displayname})) - - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - try: - content = json.loads(request.content.read()) - new_name = content["displayname"] - except: - defer.returnValue((400, "Unable to parse name")) - - yield self.handlers.profile_handler.set_displayname( - user, auth_user, new_name) - - defer.returnValue((200, {})) - - def on_OPTIONS(self, request, user_id): - return (200, {}) - - -class ProfileAvatarURLRestServlet(RestServlet): - PATTERN = client_path_pattern("/profile/(?P[^/]*)/avatar_url") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) - - avatar_url = yield self.handlers.profile_handler.get_avatar_url( - user, - ) - - defer.returnValue((200, {"avatar_url": avatar_url})) - - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - try: - content = json.loads(request.content.read()) - new_name = content["avatar_url"] - except: - defer.returnValue((400, "Unable to parse name")) - - yield self.handlers.profile_handler.set_avatar_url( - user, auth_user, new_name) - - defer.returnValue((200, {})) - - def on_OPTIONS(self, request, user_id): - return (200, {}) - - -class ProfileRestServlet(RestServlet): - PATTERN = client_path_pattern("/profile/(?P[^/]*)") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) - - displayname = yield self.handlers.profile_handler.get_displayname( - user, - ) - avatar_url = yield self.handlers.profile_handler.get_avatar_url( - user, - ) - - defer.returnValue((200, { - "displayname": displayname, - "avatar_url": avatar_url - })) - - -def register_servlets(hs, http_server): - ProfileDisplaynameRestServlet(hs).register(http_server) - ProfileAvatarURLRestServlet(hs).register(http_server) - ProfileRestServlet(hs).register(http_server) diff --git a/synapse/rest/register.py b/synapse/rest/register.py deleted file mode 100644 index e3b26902d9..0000000000 --- a/synapse/rest/register.py +++ /dev/null @@ -1,291 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains REST servlets to do with registration: /register""" -from twisted.internet import defer - -from synapse.api.errors import SynapseError, Codes -from synapse.api.constants import LoginType -from base import RestServlet, client_path_pattern -import synapse.util.stringutils as stringutils - -from synapse.util.async import run_on_reactor - -from hashlib import sha1 -import hmac -import json -import logging -import urllib - -logger = logging.getLogger(__name__) - - -# We ought to be using hmac.compare_digest() but on older pythons it doesn't -# exist. It's a _really minor_ security flaw to use plain string comparison -# because the timing attack is so obscured by all the other code here it's -# unlikely to make much difference -if hasattr(hmac, "compare_digest"): - compare_digest = hmac.compare_digest -else: - compare_digest = lambda a, b: a == b - - -class RegisterRestServlet(RestServlet): - """Handles registration with the home server. - - This servlet is in control of the registration flow; the registration - handler doesn't have a concept of multi-stages or sessions. - """ - - PATTERN = client_path_pattern("/register$") - - def __init__(self, hs): - super(RegisterRestServlet, self).__init__(hs) - # sessions are stored as: - # self.sessions = { - # "session_id" : { __session_dict__ } - # } - # TODO: persistent storage - self.sessions = {} - - def on_GET(self, request): - if self.hs.config.enable_registration_captcha: - return ( - 200, - {"flows": [ - { - "type": LoginType.RECAPTCHA, - "stages": [ - LoginType.RECAPTCHA, - LoginType.EMAIL_IDENTITY, - LoginType.PASSWORD - ] - }, - { - "type": LoginType.RECAPTCHA, - "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] - } - ]} - ) - else: - return ( - 200, - {"flows": [ - { - "type": LoginType.EMAIL_IDENTITY, - "stages": [ - LoginType.EMAIL_IDENTITY, LoginType.PASSWORD - ] - }, - { - "type": LoginType.PASSWORD - } - ]} - ) - - @defer.inlineCallbacks - def on_POST(self, request): - register_json = _parse_json(request) - - session = (register_json["session"] - if "session" in register_json else None) - login_type = None - if "type" not in register_json: - raise SynapseError(400, "Missing 'type' key.") - - try: - login_type = register_json["type"] - stages = { - LoginType.RECAPTCHA: self._do_recaptcha, - LoginType.PASSWORD: self._do_password, - LoginType.EMAIL_IDENTITY: self._do_email_identity - } - - session_info = self._get_session_info(request, session) - logger.debug("%s : session info %s request info %s", - login_type, session_info, register_json) - response = yield stages[login_type]( - request, - register_json, - session_info - ) - - if "access_token" not in response: - # isn't a final response - response["session"] = session_info["id"] - - defer.returnValue((200, response)) - except KeyError as e: - logger.exception(e) - raise SynapseError(400, "Missing JSON keys for login type %s." % ( - login_type, - )) - - def on_OPTIONS(self, request): - return (200, {}) - - def _get_session_info(self, request, session_id): - if not session_id: - # create a new session - while session_id is None or session_id in self.sessions: - session_id = stringutils.random_string(24) - self.sessions[session_id] = { - "id": session_id, - LoginType.EMAIL_IDENTITY: False, - LoginType.RECAPTCHA: False - } - - return self.sessions[session_id] - - def _save_session(self, session): - # TODO: Persistent storage - logger.debug("Saving session %s", session) - self.sessions[session["id"]] = session - - def _remove_session(self, session): - logger.debug("Removing session %s", session) - self.sessions.pop(session["id"]) - - @defer.inlineCallbacks - def _do_recaptcha(self, request, register_json, session): - if not self.hs.config.enable_registration_captcha: - raise SynapseError(400, "Captcha not required.") - - yield self._check_recaptcha(request, register_json, session) - - session[LoginType.RECAPTCHA] = True # mark captcha as done - self._save_session(session) - defer.returnValue({ - "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY] - }) - - @defer.inlineCallbacks - def _check_recaptcha(self, request, register_json, session): - if ("captcha_bypass_hmac" in register_json and - self.hs.config.captcha_bypass_secret): - if "user" not in register_json: - raise SynapseError(400, "Captcha bypass needs 'user'") - - want = hmac.new( - key=self.hs.config.captcha_bypass_secret, - msg=register_json["user"], - digestmod=sha1, - ).hexdigest() - - # str() because otherwise hmac complains that 'unicode' does not - # have the buffer interface - got = str(register_json["captcha_bypass_hmac"]) - - if compare_digest(want, got): - session["user"] = register_json["user"] - defer.returnValue(None) - else: - raise SynapseError( - 400, "Captcha bypass HMAC incorrect", - errcode=Codes.CAPTCHA_NEEDED - ) - - challenge = None - user_response = None - try: - challenge = register_json["challenge"] - user_response = register_json["response"] - except KeyError: - raise SynapseError(400, "Captcha response is required", - errcode=Codes.CAPTCHA_NEEDED) - - ip_addr = self.hs.get_ip_from_request(request) - - handler = self.handlers.registration_handler - yield handler.check_recaptcha( - ip_addr, - self.hs.config.recaptcha_private_key, - challenge, - user_response - ) - - @defer.inlineCallbacks - def _do_email_identity(self, request, register_json, session): - if (self.hs.config.enable_registration_captcha and - not session[LoginType.RECAPTCHA]): - raise SynapseError(400, "Captcha is required.") - - threepidCreds = register_json['threepidCreds'] - handler = self.handlers.registration_handler - logger.debug("Registering email. threepidcreds: %s" % (threepidCreds)) - yield handler.register_email(threepidCreds) - session["threepidCreds"] = threepidCreds # store creds for next stage - session[LoginType.EMAIL_IDENTITY] = True # mark email as done - self._save_session(session) - defer.returnValue({ - "next": LoginType.PASSWORD - }) - - @defer.inlineCallbacks - def _do_password(self, request, register_json, session): - yield run_on_reactor() - if (self.hs.config.enable_registration_captcha and - not session[LoginType.RECAPTCHA]): - # captcha should've been done by this stage! - raise SynapseError(400, "Captcha is required.") - - if ("user" in session and "user" in register_json and - session["user"] != register_json["user"]): - raise SynapseError( - 400, "Cannot change user ID during registration" - ) - - password = register_json["password"].encode("utf-8") - desired_user_id = (register_json["user"].encode("utf-8") - if "user" in register_json else None) - if (desired_user_id - and urllib.quote(desired_user_id) != desired_user_id): - raise SynapseError( - 400, - "User ID must only contain characters which do not " + - "require URL encoding.") - handler = self.handlers.registration_handler - (user_id, token) = yield handler.register( - localpart=desired_user_id, - password=password - ) - - if session[LoginType.EMAIL_IDENTITY]: - logger.debug("Binding emails %s to %s" % ( - session["threepidCreds"], user_id) - ) - yield handler.bind_emails(user_id, session["threepidCreds"]) - - result = { - "user_id": user_id, - "access_token": token, - "home_server": self.hs.hostname, - } - self._remove_session(session) - defer.returnValue(result) - - -def _parse_json(request): - try: - content = json.loads(request.content.read()) - if type(content) != dict: - raise SynapseError(400, "Content must be a JSON object.") - return content - except ValueError: - raise SynapseError(400, "Content not JSON.") - - -def register_servlets(hs, http_server): - RegisterRestServlet(hs).register(http_server) diff --git a/synapse/rest/room.py b/synapse/rest/room.py deleted file mode 100644 index 48bba2a5f3..0000000000 --- a/synapse/rest/room.py +++ /dev/null @@ -1,559 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with rooms: /rooms/ """ -from twisted.internet import defer - -from base import RestServlet, client_path_pattern -from synapse.api.errors import SynapseError, Codes -from synapse.streams.config import PaginationConfig -from synapse.api.constants import EventTypes, Membership - -import json -import logging -import urllib - - -logger = logging.getLogger(__name__) - - -class RoomCreateRestServlet(RestServlet): - # No PATTERN; we have custom dispatch rules here - - def register(self, http_server): - 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, txn_id): - try: - defer.returnValue( - self.txns.get_client_transaction(request, txn_id) - ) - except KeyError: - pass - - response = yield self.on_POST(request) - - self.txns.store_client_transaction(request, txn_id, response) - defer.returnValue(response) - - @defer.inlineCallbacks - def on_POST(self, request): - auth_user = yield self.auth.get_user_by_req(request) - - room_config = self.get_room_config(request) - info = yield self.make_room(room_config, auth_user, None) - room_config.update(info) - defer.returnValue((200, info)) - - @defer.inlineCallbacks - def make_room(self, room_config, auth_user, room_id): - handler = self.handlers.room_creation_handler - info = yield handler.create_room( - user_id=auth_user.to_string(), - room_id=room_id, - config=room_config - ) - defer.returnValue(info) - - def get_room_config(self, request): - try: - user_supplied_config = json.loads(request.content.read()) - if "visibility" not in user_supplied_config: - # default visibility - user_supplied_config["visibility"] = "public" - return user_supplied_config - except (ValueError, TypeError): - raise SynapseError(400, "Body must be JSON.", - errcode=Codes.BAD_JSON) - - def on_OPTIONS(self, request): - return (200, {}) - - -# TODO: Needs unit testing for generic events -class RoomStateEventRestServlet(RestServlet): - def register(self, http_server): - # /room/$roomid/state/$eventtype - no_state_key = "/rooms/(?P[^/]*)/state/(?P[^/]*)$" - - # /room/$roomid/state/$eventtype/$statekey - state_key = ("/rooms/(?P[^/]*)/state/" - "(?P[^/]*)/(?P[^/]*)$") - - http_server.register_path("GET", - client_path_pattern(state_key), - self.on_GET) - http_server.register_path("PUT", - client_path_pattern(state_key), - self.on_PUT) - http_server.register_path("GET", - client_path_pattern(no_state_key), - self.on_GET_no_state_key) - http_server.register_path("PUT", - client_path_pattern(no_state_key), - self.on_PUT_no_state_key) - - def on_GET_no_state_key(self, request, room_id, event_type): - return self.on_GET(request, room_id, event_type, "") - - def on_PUT_no_state_key(self, request, room_id, event_type): - return self.on_PUT(request, room_id, event_type, "") - - @defer.inlineCallbacks - def on_GET(self, request, room_id, event_type, state_key): - user = yield self.auth.get_user_by_req(request) - - msg_handler = self.handlers.message_handler - data = yield msg_handler.get_room_data( - user_id=user.to_string(), - room_id=room_id, - event_type=event_type, - state_key=state_key, - ) - - if not data: - raise SynapseError( - 404, "Event not found.", errcode=Codes.NOT_FOUND - ) - defer.returnValue((200, data.get_dict()["content"])) - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, event_type, state_key): - user = yield self.auth.get_user_by_req(request) - - content = _parse_json(request) - - event_dict = { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": user.to_string(), - } - - if state_key is not None: - event_dict["state_key"] = state_key - - msg_handler = self.handlers.message_handler - yield msg_handler.create_and_send_event(event_dict) - - defer.returnValue((200, {})) - - -# TODO: Needs unit testing for generic events + feedback -class RoomSendEventRestServlet(RestServlet): - - def register(self, http_server): - # /rooms/$roomid/send/$event_type[/$txn_id] - PATTERN = ("/rooms/(?P[^/]*)/send/(?P[^/]*)") - register_txn_path(self, PATTERN, http_server, with_get=True) - - @defer.inlineCallbacks - def on_POST(self, request, room_id, event_type): - user = yield self.auth.get_user_by_req(request) - content = _parse_json(request) - - msg_handler = self.handlers.message_handler - event = yield msg_handler.create_and_send_event( - { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": user.to_string(), - } - ) - - defer.returnValue((200, {"event_id": event.event_id})) - - def on_GET(self, request, room_id, event_type, txn_id): - return (200, "Not implemented") - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, event_type, txn_id): - try: - defer.returnValue( - self.txns.get_client_transaction(request, txn_id) - ) - except KeyError: - pass - - response = yield self.on_POST(request, room_id, event_type) - - self.txns.store_client_transaction(request, txn_id, response) - defer.returnValue(response) - - -# TODO: Needs unit testing for room ID + alias joins -class JoinRoomAliasServlet(RestServlet): - - def register(self, http_server): - # /join/$room_identifier[/$txn_id] - PATTERN = ("/join/(?P[^/]*)") - register_txn_path(self, PATTERN, http_server) - - @defer.inlineCallbacks - def on_POST(self, request, room_identifier): - user = yield self.auth.get_user_by_req(request) - - # 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. - - identifier = None - is_room_alias = False - try: - identifier = self.hs.parse_roomalias(room_identifier) - is_room_alias = True - except SynapseError: - identifier = self.hs.parse_roomid(room_identifier) - - # TODO: Support for specifying the home server to join with? - - 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 - msg_handler = self.handlers.message_handler - yield msg_handler.create_and_send_event( - { - "type": EventTypes.Member, - "content": {"membership": Membership.JOIN}, - "room_id": identifier.to_string(), - "sender": user.to_string(), - "state_key": user.to_string(), - } - ) - - defer.returnValue((200, {"room_id": identifier.to_string()})) - - @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 -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[^/]*)/members$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - # TODO support Pagination stream API (limit/tokens) - user = yield self.auth.get_user_by_req(request) - handler = self.handlers.room_member_handler - members = yield handler.get_room_members_as_pagination_chunk( - room_id=room_id, - user_id=user.to_string()) - - for event in members["chunk"]: - # FIXME: should probably be state_key here, not user_id - target_user = self.hs.parse_userid(event["user_id"]) - # Presence is an optional cache; don't fail if we can't fetch it - try: - presence_handler = self.handlers.presence_handler - presence_state = yield presence_handler.get_state( - target_user=target_user, auth_user=user - ) - event["content"].update(presence_state) - except: - pass - - defer.returnValue((200, members)) - - -# TODO: Needs unit testing -class RoomMessageListRestServlet(RestServlet): - PATTERN = client_path_pattern("/rooms/(?P[^/]*)/messages$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - user = yield self.auth.get_user_by_req(request) - pagination_config = PaginationConfig.from_request( - request, default_limit=10, - ) - with_feedback = "feedback" in request.args - as_client_event = "raw" not in request.args - handler = self.handlers.message_handler - msgs = yield handler.get_messages( - room_id=room_id, - user_id=user.to_string(), - pagin_config=pagination_config, - feedback=with_feedback, - as_client_event=as_client_event - ) - - defer.returnValue((200, msgs)) - - -# TODO: Needs unit testing -class RoomStateRestServlet(RestServlet): - PATTERN = client_path_pattern("/rooms/(?P[^/]*)/state$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - user = yield self.auth.get_user_by_req(request) - handler = self.handlers.message_handler - # Get all the current state for this room - events = yield handler.get_state_events( - room_id=room_id, - user_id=user.to_string(), - ) - defer.returnValue((200, events)) - - -# TODO: Needs unit testing -class RoomInitialSyncRestServlet(RestServlet): - PATTERN = client_path_pattern("/rooms/(?P[^/]*)/initialSync$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - user = yield self.auth.get_user_by_req(request) - pagination_config = PaginationConfig.from_request(request) - content = yield self.handlers.message_handler.room_initial_sync( - room_id=room_id, - user_id=user.to_string(), - pagin_config=pagination_config, - ) - defer.returnValue((200, content)) - - -class RoomTriggerBackfill(RestServlet): - PATTERN = client_path_pattern("/rooms/(?P[^/]*)/backfill$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - remote_server = urllib.unquote( - request.args["remote"][0] - ).decode("UTF-8") - - limit = int(request.args["limit"][0]) - - handler = self.handlers.federation_handler - events = yield handler.backfill(remote_server, room_id, limit) - - res = [self.hs.serialize_event(event) for event in events] - defer.returnValue((200, res)) - - -# TODO: Needs unit testing -class RoomMembershipRestServlet(RestServlet): - - def register(self, http_server): - # /rooms/$roomid/[invite|join|leave] - PATTERN = ("/rooms/(?P[^/]*)/" - "(?Pjoin|invite|leave|ban|kick)") - register_txn_path(self, PATTERN, http_server) - - @defer.inlineCallbacks - def on_POST(self, request, room_id, membership_action): - user = yield self.auth.get_user_by_req(request) - - content = _parse_json(request) - - # target user is you unless it is an invite - state_key = user.to_string() - if membership_action in ["invite", "ban", "kick"]: - if "user_id" not in content: - raise SynapseError(400, "Missing user_id key.") - state_key = content["user_id"] - - if membership_action == "kick": - membership_action = "leave" - - msg_handler = self.handlers.message_handler - yield msg_handler.create_and_send_event( - { - "type": EventTypes.Member, - "content": {"membership": unicode(membership_action)}, - "room_id": room_id, - "sender": user.to_string(), - "state_key": state_key, - } - ) - - defer.returnValue((200, {})) - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, membership_action, txn_id): - try: - defer.returnValue( - self.txns.get_client_transaction(request, txn_id) - ) - except KeyError: - pass - - response = yield self.on_POST(request, room_id, membership_action) - - self.txns.store_client_transaction(request, txn_id, response) - defer.returnValue(response) - - -class RoomRedactEventRestServlet(RestServlet): - def register(self, http_server): - PATTERN = ("/rooms/(?P[^/]*)/redact/(?P[^/]*)") - register_txn_path(self, PATTERN, http_server) - - @defer.inlineCallbacks - def on_POST(self, request, room_id, event_id): - user = yield self.auth.get_user_by_req(request) - content = _parse_json(request) - - msg_handler = self.handlers.message_handler - event = yield msg_handler.create_and_send_event( - { - "type": EventTypes.Redaction, - "content": content, - "room_id": room_id, - "sender": user.to_string(), - "redacts": event_id, - } - ) - - defer.returnValue((200, {"event_id": event.event_id})) - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, event_id, txn_id): - try: - defer.returnValue( - self.txns.get_client_transaction(request, txn_id) - ) - except KeyError: - pass - - response = yield self.on_POST(request, room_id, event_id) - - self.txns.store_client_transaction(request, txn_id, response) - defer.returnValue(response) - - -class RoomTypingRestServlet(RestServlet): - PATTERN = client_path_pattern( - "/rooms/(?P[^/]*)/typing/(?P[^/]*)$" - ) - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, user_id): - auth_user = yield self.auth.get_user_by_req(request) - - room_id = urllib.unquote(room_id) - target_user = self.hs.parse_userid(urllib.unquote(user_id)) - - content = _parse_json(request) - - typing_handler = self.handlers.typing_notification_handler - - if content["typing"]: - yield typing_handler.started_typing( - target_user=target_user, - auth_user=auth_user, - room_id=room_id, - timeout=content.get("timeout", 30000), - ) - else: - yield typing_handler.stopped_typing( - target_user=target_user, - auth_user=auth_user, - room_id=room_id, - ) - - defer.returnValue((200, {})) - - -def _parse_json(request): - try: - content = json.loads(request.content.read()) - if type(content) != dict: - raise SynapseError(400, "Content must be a JSON object.", - errcode=Codes.NOT_JSON) - return content - except ValueError: - raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) - - -def register_txn_path(servlet, regex_string, http_server, with_get=False): - """Registers a transaction-based path. - - This registers two paths: - PUT regex_string/$txnid - POST regex_string - - Args: - regex_string (str): The regex string to register. Must NOT have a - trailing $ as this string will be appended to. - http_server : The http_server to register paths with. - with_get: True to also register respective GET paths for the PUTs. - """ - http_server.register_path( - "POST", - client_path_pattern(regex_string + "$"), - servlet.on_POST - ) - http_server.register_path( - "PUT", - client_path_pattern(regex_string + "/(?P[^/]*)$"), - servlet.on_PUT - ) - if with_get: - http_server.register_path( - "GET", - client_path_pattern(regex_string + "/(?P[^/]*)$"), - servlet.on_GET - ) - - -def register_servlets(hs, http_server): - RoomStateEventRestServlet(hs).register(http_server) - RoomCreateRestServlet(hs).register(http_server) - RoomMemberListRestServlet(hs).register(http_server) - RoomMessageListRestServlet(hs).register(http_server) - JoinRoomAliasServlet(hs).register(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) - RoomRedactEventRestServlet(hs).register(http_server) - RoomTypingRestServlet(hs).register(http_server) diff --git a/synapse/rest/transactions.py b/synapse/rest/transactions.py deleted file mode 100644 index d933fea18a..0000000000 --- a/synapse/rest/transactions.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains logic for storing HTTP PUT transactions. This is used -to ensure idempotency when performing PUTs using the REST API.""" -import logging - -logger = logging.getLogger(__name__) - - -# FIXME: elsewhere we use FooStore to indicate something in the storage layer... -class HttpTransactionStore(object): - - def __init__(self): - # { key : (txn_id, response) } - self.transactions = {} - - def get_response(self, key, txn_id): - """Retrieve a response for this request. - - Args: - key (str): A transaction-independent key for this request. Usually - this is a combination of the path (without the transaction id) - and the user's access token. - txn_id (str): The transaction ID for this request - Returns: - A tuple of (HTTP response code, response content) or None. - """ - try: - logger.debug("get_response Key: %s TxnId: %s", key, txn_id) - (last_txn_id, response) = self.transactions[key] - if txn_id == last_txn_id: - logger.info("get_response: Returning a response for %s", key) - return response - except KeyError: - pass - return None - - def store_response(self, key, txn_id, response): - """Stores an HTTP response tuple. - - Args: - key (str): A transaction-independent key for this request. Usually - this is a combination of the path (without the transaction id) - and the user's access token. - txn_id (str): The transaction ID for this request. - response (tuple): A tuple of (HTTP response code, response content) - """ - logger.debug("store_response Key: %s TxnId: %s", key, txn_id) - self.transactions[key] = (txn_id, response) - - def store_client_transaction(self, request, txn_id, response): - """Stores the request/response pair of an HTTP transaction. - - Args: - request (twisted.web.http.Request): The twisted HTTP request. This - request must have the transaction ID as the last path segment. - response (tuple): A tuple of (response code, response dict) - txn_id (str): The transaction ID for this request. - """ - self.store_response(self._get_key(request), txn_id, response) - - def get_client_transaction(self, request, txn_id): - """Retrieves a stored response if there was one. - - Args: - request (twisted.web.http.Request): The twisted HTTP request. This - request must have the transaction ID as the last path segment. - txn_id (str): The transaction ID for this request. - Returns: - The response tuple. - Raises: - KeyError if the transaction was not found. - """ - response = self.get_response(self._get_key(request), txn_id) - if response is None: - raise KeyError("Transaction not found.") - return response - - def _get_key(self, request): - token = request.args["access_token"][0] - path_without_txn_id = request.path.rsplit("/", 1)[0] - return path_without_txn_id + "/" + token diff --git a/synapse/rest/voip.py b/synapse/rest/voip.py deleted file mode 100644 index 011c35e69b..0000000000 --- a/synapse/rest/voip.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.internet import defer - -from base import RestServlet, client_path_pattern - - -import hmac -import hashlib -import base64 - - -class VoipRestServlet(RestServlet): - PATTERN = client_path_pattern("/voip/turnServer$") - - @defer.inlineCallbacks - def on_GET(self, request): - auth_user = yield self.auth.get_user_by_req(request) - - turnUris = self.hs.config.turn_uris - turnSecret = self.hs.config.turn_shared_secret - userLifetime = self.hs.config.turn_user_lifetime - if not turnUris or not turnSecret or not userLifetime: - defer.returnValue((200, {})) - - expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000 - username = "%d:%s" % (expiry, auth_user.to_string()) - - mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1) - # We need to use standard base64 encoding here, *not* syutil's - # encode_base64 because we need to add the standard padding to get the - # same result as the TURN server. - password = base64.b64encode(mac.digest()) - - defer.returnValue((200, { - 'username': username, - 'password': password, - 'ttl': userLifetime / 1000, - 'uris': turnUris, - })) - - def on_OPTIONS(self, request): - return (200, {}) - - -def register_servlets(hs, http_server): - VoipRestServlet(hs).register(http_server) diff --git a/synapse/server.py b/synapse/server.py index d861efd2fd..57a95bf753 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -24,7 +24,7 @@ from synapse.events.utils import serialize_event from synapse.notifier import Notifier from synapse.api.auth import Auth from synapse.handlers import Handlers -from synapse.rest import RestServletFactory +from synapse.client.v1 import RestServletFactory from synapse.state import StateHandler from synapse.storage import DataStore from synapse.types import UserID, RoomAlias, RoomID, EventID diff --git a/tests/client/__init__.py b/tests/client/__init__.py new file mode 100644 index 0000000000..1a84d94cd9 --- /dev/null +++ b/tests/client/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/client/v1/__init__.py b/tests/client/v1/__init__.py new file mode 100644 index 0000000000..9bff9ec169 --- /dev/null +++ b/tests/client/v1/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/tests/client/v1/test_events.py b/tests/client/v1/test_events.py new file mode 100644 index 0000000000..9b36dd3225 --- /dev/null +++ b/tests/client/v1/test_events.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Tests REST events for /events paths.""" +from tests import unittest + +# twisted imports +from twisted.internet import defer + +import synapse.client.v1.events +import synapse.client.v1.register +import synapse.client.v1.room + +from synapse.server import HomeServer + +from ...utils import MockHttpResource, SQLiteMemoryDbPool, MockKey +from .utils import RestTestCase + +from mock import Mock, NonCallableMock + + +PATH_PREFIX = "/_matrix/client/api/v1" + + +class EventStreamPaginationApiTestCase(unittest.TestCase): + """ Tests event streaming query parameters and start/end keys used in the + Pagination stream API. """ + user_id = "sid1" + + def setUp(self): + # configure stream and inject items + pass + + def tearDown(self): + pass + + def TODO_test_long_poll(self): + # stream from 'end' key, send (self+other) message, expect message. + + # stream from 'END', send (self+other) message, expect message. + + # stream from 'end' key, send (self+other) topic, expect topic. + + # stream from 'END', send (self+other) topic, expect topic. + + # stream from 'end' key, send (self+other) invite, expect invite. + + # stream from 'END', send (self+other) invite, expect invite. + + pass + + def TODO_test_stream_forward(self): + # stream from START, expect injected items + + # stream from 'start' key, expect same content + + # stream from 'end' key, expect nothing + + # stream from 'END', expect nothing + + # The following is needed for cases where content is removed e.g. you + # left a room, so the token you're streaming from is > the one that + # would be returned naturally from START>END. + # stream from very new token (higher than end key), expect same token + # returned as end key + pass + + def TODO_test_limits(self): + # stream from a key, expect limit_num items + + # stream from START, expect limit_num items + + pass + + def TODO_test_range(self): + # stream from key to key, expect X items + + # stream from key to END, expect X items + + # stream from START to key, expect X items + + # stream from START to END, expect all items + pass + + def TODO_test_direction(self): + # stream from END to START and fwds, expect newest first + + # stream from END to START and bwds, expect oldest first + + # stream from START to END and fwds, expect oldest first + + # stream from START to END and bwds, expect newest first + + pass + + +class EventStreamPermissionsTestCase(RestTestCase): + """ Tests event streaming (GET /events). """ + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "test", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + clock=Mock(spec=[ + "call_later", + "cancel_call_later", + "time_msec", + "time" + ]), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + hs.config.enable_registration_captcha = False + + hs.get_handlers().federation_handler = Mock() + + hs.get_clock().time_msec.return_value = 1000000 + hs.get_clock().time.return_value = 1000 + + synapse.client.v1.register.register_servlets(hs, self.mock_resource) + synapse.client.v1.events.register_servlets(hs, self.mock_resource) + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + # register an account + self.user_id = "sid1" + response = yield self.register(self.user_id) + self.token = response["access_token"] + self.user_id = response["user_id"] + + # register a 2nd account + self.other_user = "other1" + response = yield self.register(self.other_user) + self.other_token = response["access_token"] + self.other_user = response["user_id"] + + def tearDown(self): + pass + + @defer.inlineCallbacks + def test_stream_basic_permissions(self): + # invalid token, expect 403 + (code, response) = yield self.mock_resource.trigger_get( + "/events?access_token=%s" % ("invalid" + self.token, ) + ) + self.assertEquals(403, code, msg=str(response)) + + # valid token, expect content + (code, response) = yield self.mock_resource.trigger_get( + "/events?access_token=%s&timeout=0" % (self.token,) + ) + self.assertEquals(200, code, msg=str(response)) + self.assertTrue("chunk" in response) + self.assertTrue("start" in response) + self.assertTrue("end" in response) + + @defer.inlineCallbacks + def test_stream_room_permissions(self): + 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) + yield self.invite( + room_id, + src=self.other_user, + targ=self.user_id, + tok=self.other_token + ) + + (code, response) = yield self.mock_resource.trigger_get( + "/events?access_token=%s&timeout=0" % (self.token,) + ) + self.assertEquals(200, code, msg=str(response)) + + self.assertEquals(0, len(response["chunk"])) + + # joined room (expect all content for room) + yield self.join(room=room_id, user=self.user_id, tok=self.token) + + # left to room (expect no content for room) + + def TODO_test_stream_items(self): + # new user, no content + + # join room, expect 1 item (join) + + # send message, expect 2 items (join,send) + + # set topic, expect 3 items (join,send,topic) + + # someone else join room, expect 4 (join,send,topic,join) + + # someone else send message, expect 5 (join,send.topic,join,send) + + # someone else set topic, expect 6 (join,send,topic,join,send,topic) + pass diff --git a/tests/client/v1/test_presence.py b/tests/client/v1/test_presence.py new file mode 100644 index 0000000000..e7d636c74d --- /dev/null +++ b/tests/client/v1/test_presence.py @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests REST events for /presence paths.""" + +from tests import unittest +from twisted.internet import defer + +from mock import Mock + +from ...utils import MockHttpResource, MockKey + +from synapse.api.constants import PresenceState +from synapse.handlers.presence import PresenceHandler +from synapse.server import HomeServer + + +OFFLINE = PresenceState.OFFLINE +UNAVAILABLE = PresenceState.UNAVAILABLE +ONLINE = PresenceState.ONLINE + + +myid = "@apple:test" +PATH_PREFIX = "/_matrix/client/api/v1" + + +class JustPresenceHandlers(object): + def __init__(self, hs): + self.presence_handler = PresenceHandler(hs) + + +class PresenceStateTestCase(unittest.TestCase): + + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.mock_config = Mock() + self.mock_config.signing_key = [MockKey()] + hs = HomeServer("test", + db_pool=None, + datastore=Mock(spec=[ + "get_presence_state", + "set_presence_state", + "insert_client_ip", + ]), + http_client=None, + resource_for_client=self.mock_resource, + resource_for_federation=self.mock_resource, + config=self.mock_config, + ) + hs.handlers = JustPresenceHandlers(hs) + + self.datastore = hs.get_datastore() + + def get_presence_list(*a, **kw): + return defer.succeed([]) + self.datastore.get_presence_list = get_presence_list + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(myid), + "admin": False, + "device_id": None, + } + + hs.get_auth().get_user_by_token = _get_user_by_token + + room_member_handler = hs.handlers.room_member_handler = Mock( + spec=[ + "get_rooms_for_user", + ] + ) + + def get_rooms_for_user(user): + return defer.succeed([]) + room_member_handler.get_rooms_for_user = get_rooms_for_user + + hs.register_servlets() + + self.u_apple = hs.parse_userid(myid) + + @defer.inlineCallbacks + def test_get_my_status(self): + mocked_get = self.datastore.get_presence_state + mocked_get.return_value = defer.succeed( + {"state": ONLINE, "status_msg": "Available"} + ) + + (code, response) = yield self.mock_resource.trigger("GET", + "/presence/%s/status" % (myid), None) + + self.assertEquals(200, code) + self.assertEquals( + {"presence": ONLINE, "status_msg": "Available"}, + response + ) + mocked_get.assert_called_with("apple") + + @defer.inlineCallbacks + def test_set_my_status(self): + mocked_set = self.datastore.set_presence_state + mocked_set.return_value = defer.succeed({"state": OFFLINE}) + + (code, response) = yield self.mock_resource.trigger("PUT", + "/presence/%s/status" % (myid), + '{"presence": "unavailable", "status_msg": "Away"}') + + self.assertEquals(200, code) + mocked_set.assert_called_with("apple", + {"state": UNAVAILABLE, "status_msg": "Away"} + ) + + +class PresenceListTestCase(unittest.TestCase): + + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.mock_config = Mock() + self.mock_config.signing_key = [MockKey()] + + hs = HomeServer("test", + db_pool=None, + datastore=Mock(spec=[ + "has_presence_state", + "get_presence_state", + "allow_presence_visible", + "is_presence_visible", + "add_presence_list_pending", + "set_presence_list_accepted", + "del_presence_list", + "get_presence_list", + "insert_client_ip", + ]), + http_client=None, + resource_for_client=self.mock_resource, + resource_for_federation=self.mock_resource, + config=self.mock_config, + ) + hs.handlers = JustPresenceHandlers(hs) + + self.datastore = hs.get_datastore() + + def has_presence_state(user_localpart): + return defer.succeed( + user_localpart in ("apple", "banana",) + ) + self.datastore.has_presence_state = has_presence_state + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(myid), + "admin": False, + "device_id": None, + } + + room_member_handler = hs.handlers.room_member_handler = Mock( + spec=[ + "get_rooms_for_user", + ] + ) + + hs.get_auth().get_user_by_token = _get_user_by_token + + hs.register_servlets() + + self.u_apple = hs.parse_userid("@apple:test") + self.u_banana = hs.parse_userid("@banana:test") + + @defer.inlineCallbacks + def test_get_my_list(self): + self.datastore.get_presence_list.return_value = defer.succeed( + [{"observed_user_id": "@banana:test"}], + ) + + (code, response) = yield self.mock_resource.trigger("GET", + "/presence/list/%s" % (myid), None) + + self.assertEquals(200, code) + self.assertEquals([ + {"user_id": "@banana:test", "presence": OFFLINE}, + ], response) + + self.datastore.get_presence_list.assert_called_with( + "apple", accepted=True + ) + + @defer.inlineCallbacks + def test_invite(self): + self.datastore.add_presence_list_pending.return_value = ( + defer.succeed(()) + ) + self.datastore.is_presence_visible.return_value = defer.succeed( + True + ) + + (code, response) = yield self.mock_resource.trigger("POST", + "/presence/list/%s" % (myid), + """{"invite": ["@banana:test"]}""" + ) + + self.assertEquals(200, code) + + self.datastore.add_presence_list_pending.assert_called_with( + "apple", "@banana:test" + ) + self.datastore.set_presence_list_accepted.assert_called_with( + "apple", "@banana:test" + ) + + @defer.inlineCallbacks + def test_drop(self): + self.datastore.del_presence_list.return_value = ( + defer.succeed(()) + ) + + (code, response) = yield self.mock_resource.trigger("POST", + "/presence/list/%s" % (myid), + """{"drop": ["@banana:test"]}""" + ) + + self.assertEquals(200, code) + + self.datastore.del_presence_list.assert_called_with( + "apple", "@banana:test" + ) + + +class PresenceEventStreamTestCase(unittest.TestCase): + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + self.mock_config = Mock() + self.mock_config.signing_key = [MockKey()] + + # HIDEOUS HACKERY + # TODO(paul): This should be injected in via the HomeServer DI system + from synapse.streams.events import ( + PresenceEventSource, NullSource, EventSources + ) + + old_SOURCE_TYPES = EventSources.SOURCE_TYPES + def tearDown(): + EventSources.SOURCE_TYPES = old_SOURCE_TYPES + self.tearDown = tearDown + + EventSources.SOURCE_TYPES = { + k: NullSource for k in old_SOURCE_TYPES.keys() + } + EventSources.SOURCE_TYPES["presence"] = PresenceEventSource + + hs = HomeServer("test", + db_pool=None, + http_client=None, + resource_for_client=self.mock_resource, + resource_for_federation=self.mock_resource, + datastore=Mock(spec=[ + "set_presence_state", + "get_presence_list", + ]), + clock=Mock(spec=[ + "call_later", + "cancel_call_later", + "time_msec", + ]), + config=self.mock_config, + ) + + hs.get_clock().time_msec.return_value = 1000000 + + def _get_user_by_req(req=None): + return hs.parse_userid(myid) + + hs.get_auth().get_user_by_req = _get_user_by_req + + hs.register_servlets() + + hs.handlers.room_member_handler = Mock(spec=[]) + + self.room_members = [] + + def get_rooms_for_user(user): + if user in self.room_members: + return ["a-room"] + else: + return [] + hs.handlers.room_member_handler.get_rooms_for_user = get_rooms_for_user + + self.mock_datastore = hs.get_datastore() + + def get_profile_displayname(user_id): + return defer.succeed("Frank") + self.mock_datastore.get_profile_displayname = get_profile_displayname + + def get_profile_avatar_url(user_id): + return defer.succeed(None) + self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url + + def user_rooms_intersect(user_list): + room_member_ids = map(lambda u: u.to_string(), self.room_members) + + shared = all(map(lambda i: i in room_member_ids, user_list)) + return defer.succeed(shared) + self.mock_datastore.user_rooms_intersect = user_rooms_intersect + + def get_joined_hosts_for_room(room_id): + return [] + self.mock_datastore.get_joined_hosts_for_room = get_joined_hosts_for_room + + self.presence = hs.get_handlers().presence_handler + + self.u_apple = hs.parse_userid("@apple:test") + self.u_banana = hs.parse_userid("@banana:test") + + @defer.inlineCallbacks + def test_shortpoll(self): + self.room_members = [self.u_apple, self.u_banana] + + self.mock_datastore.set_presence_state.return_value = defer.succeed( + {"state": ONLINE} + ) + self.mock_datastore.get_presence_list.return_value = defer.succeed( + [] + ) + + (code, response) = yield self.mock_resource.trigger("GET", + "/events?timeout=0", None) + + self.assertEquals(200, code) + + # We've forced there to be only one data stream so the tokens will + # all be ours + + # I'll already get my own presence state change + self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []}, + response + ) + + self.mock_datastore.set_presence_state.return_value = defer.succeed( + {"state": ONLINE} + ) + self.mock_datastore.get_presence_list.return_value = defer.succeed( + [] + ) + + yield self.presence.set_state(self.u_banana, self.u_banana, + state={"presence": ONLINE} + ) + + (code, response) = yield self.mock_resource.trigger("GET", + "/events?from=0_1_0&timeout=0", None) + + self.assertEquals(200, code) + self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [ + {"type": "m.presence", + "content": { + "user_id": "@banana:test", + "presence": ONLINE, + "displayname": "Frank", + "last_active_ago": 0, + }}, + ]}, response) diff --git a/tests/client/v1/test_profile.py b/tests/client/v1/test_profile.py new file mode 100644 index 0000000000..1182cc54eb --- /dev/null +++ b/tests/client/v1/test_profile.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests REST events for /profile paths.""" + +from tests import unittest +from twisted.internet import defer + +from mock import Mock, NonCallableMock + +from ...utils import MockHttpResource, MockKey + +from synapse.api.errors import SynapseError, AuthError +from synapse.server import HomeServer + +myid = "@1234ABCD:test" +PATH_PREFIX = "/_matrix/client/api/v1" + + +class ProfileTestCase(unittest.TestCase): + """ Tests profile management. """ + + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.mock_handler = Mock(spec=[ + "get_displayname", + "set_displayname", + "get_avatar_url", + "set_avatar_url", + ]) + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + hs = HomeServer("test", + db_pool=None, + http_client=None, + resource_for_client=self.mock_resource, + federation=Mock(), + replication_layer=Mock(), + datastore=None, + config=self.mock_config, + ) + + def _get_user_by_req(request=None): + return hs.parse_userid(myid) + + hs.get_auth().get_user_by_req = _get_user_by_req + + hs.get_handlers().profile_handler = self.mock_handler + + hs.register_servlets() + + @defer.inlineCallbacks + def test_get_my_name(self): + mocked_get = self.mock_handler.get_displayname + mocked_get.return_value = defer.succeed("Frank") + + (code, response) = yield self.mock_resource.trigger("GET", + "/profile/%s/displayname" % (myid), None) + + self.assertEquals(200, code) + self.assertEquals({"displayname": "Frank"}, response) + self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD") + + @defer.inlineCallbacks + def test_set_my_name(self): + mocked_set = self.mock_handler.set_displayname + mocked_set.return_value = defer.succeed(()) + + (code, response) = yield self.mock_resource.trigger("PUT", + "/profile/%s/displayname" % (myid), + '{"displayname": "Frank Jr."}') + + self.assertEquals(200, code) + self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD") + self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD") + self.assertEquals(mocked_set.call_args[0][2], "Frank Jr.") + + @defer.inlineCallbacks + def test_set_my_name_noauth(self): + mocked_set = self.mock_handler.set_displayname + mocked_set.side_effect = AuthError(400, "message") + + (code, response) = yield self.mock_resource.trigger("PUT", + "/profile/%s/displayname" % ("@4567:test"), '"Frank Jr."') + + self.assertTrue(400 <= code < 499, + msg="code %d is in the 4xx range" % (code)) + + @defer.inlineCallbacks + def test_get_other_name(self): + mocked_get = self.mock_handler.get_displayname + mocked_get.return_value = defer.succeed("Bob") + + (code, response) = yield self.mock_resource.trigger("GET", + "/profile/%s/displayname" % ("@opaque:elsewhere"), None) + + self.assertEquals(200, code) + self.assertEquals({"displayname": "Bob"}, response) + + @defer.inlineCallbacks + def test_set_other_name(self): + mocked_set = self.mock_handler.set_displayname + mocked_set.side_effect = SynapseError(400, "message") + + (code, response) = yield self.mock_resource.trigger("PUT", + "/profile/%s/displayname" % ("@opaque:elsewhere"), None) + + self.assertTrue(400 <= code <= 499, + msg="code %d is in the 4xx range" % (code)) + + @defer.inlineCallbacks + def test_get_my_avatar(self): + mocked_get = self.mock_handler.get_avatar_url + mocked_get.return_value = defer.succeed("http://my.server/me.png") + + (code, response) = yield self.mock_resource.trigger("GET", + "/profile/%s/avatar_url" % (myid), None) + + self.assertEquals(200, code) + self.assertEquals({"avatar_url": "http://my.server/me.png"}, response) + self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD") + + @defer.inlineCallbacks + def test_set_my_avatar(self): + mocked_set = self.mock_handler.set_avatar_url + mocked_set.return_value = defer.succeed(()) + + (code, response) = yield self.mock_resource.trigger("PUT", + "/profile/%s/avatar_url" % (myid), + '{"avatar_url": "http://my.server/pic.gif"}') + + self.assertEquals(200, code) + self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD") + self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD") + self.assertEquals(mocked_set.call_args[0][2], + "http://my.server/pic.gif") diff --git a/tests/client/v1/test_rooms.py b/tests/client/v1/test_rooms.py new file mode 100644 index 0000000000..33a8631d76 --- /dev/null +++ b/tests/client/v1/test_rooms.py @@ -0,0 +1,1068 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests REST events for /rooms paths.""" + +# twisted imports +from twisted.internet import defer + +import synapse.client.v1.room +from synapse.api.constants import Membership + +from synapse.server import HomeServer + +from tests import unittest + +# python imports +import json +import urllib +import types + +from ...utils import MockHttpResource, SQLiteMemoryDbPool, MockKey +from .utils import RestTestCase + +from mock import Mock, NonCallableMock + +PATH_PREFIX = "/_matrix/client/api/v1" + + +class RoomPermissionsTestCase(RestTestCase): + """ Tests room permissions. """ + user_id = "@sid1:red" + rmcreator_id = "@notme:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + self.auth_user_id = self.rmcreator_id + + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + self.auth = hs.get_auth() + + # create some rooms under the name rmcreator_id + self.uncreated_rmid = "!aa:test" + + self.created_rmid = yield self.create_room_as(self.rmcreator_id, + is_public=False) + + 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" % + (self.created_rmid)) + (code, response) = yield self.mock_resource.trigger( + "PUT", + self.created_rmid_msg_path, + '{"msgtype":"m.text","body":"test msg"}') + self.assertEquals(200, code, msg=str(response)) + + # set topic for public room + (code, response) = yield self.mock_resource.trigger( + "PUT", + "/rooms/%s/state/m.room.topic" % self.created_public_rmid, + '{"topic":"Public Room Topic"}') + self.assertEquals(200, code, msg=str(response)) + + # auth as user_id now + self.auth_user_id = self.user_id + + def tearDown(self): + pass + +# @defer.inlineCallbacks +# def test_get_message(self): +# # get message in uncreated room, expect 403 +# (code, response) = yield self.mock_resource.trigger_get( +# "/rooms/noroom/messages/someid/m1") +# self.assertEquals(403, code, msg=str(response)) +# +# # get message in created room not joined (no state), expect 403 +# (code, response) = yield self.mock_resource.trigger_get( +# self.created_rmid_msg_path) +# self.assertEquals(403, code, msg=str(response)) +# +# # get message in created room and invited, expect 403 +# yield self.invite(room=self.created_rmid, src=self.rmcreator_id, +# targ=self.user_id) +# (code, response) = yield self.mock_resource.trigger_get( +# self.created_rmid_msg_path) +# self.assertEquals(403, code, msg=str(response)) +# +# # get message in created room and joined, expect 200 +# yield self.join(room=self.created_rmid, user=self.user_id) +# (code, response) = yield self.mock_resource.trigger_get( +# self.created_rmid_msg_path) +# self.assertEquals(200, code, msg=str(response)) +# +# # get message in created room and left, expect 403 +# yield self.leave(room=self.created_rmid, user=self.user_id) +# (code, response) = yield self.mock_resource.trigger_get( +# self.created_rmid_msg_path) +# self.assertEquals(403, code, msg=str(response)) + + @defer.inlineCallbacks + def test_send_message(self): + msg_content = '{"msgtype":"m.text","body":"hello"}' + send_msg_path = ( + "/rooms/%s/send/m.room.message/mid1" % (self.created_rmid,) + ) + + # send message in uncreated room, expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", + "/rooms/%s/send/m.room.message/mid2" % (self.uncreated_rmid,), + msg_content + ) + self.assertEquals(403, code, msg=str(response)) + + # send message in created room not joined (no state), expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", + send_msg_path, + msg_content + ) + self.assertEquals(403, code, msg=str(response)) + + # send message in created room and invited, expect 403 + yield self.invite( + room=self.created_rmid, + src=self.rmcreator_id, + targ=self.user_id + ) + (code, response) = yield self.mock_resource.trigger( + "PUT", + send_msg_path, + msg_content + ) + self.assertEquals(403, code, msg=str(response)) + + # send message in created room and joined, expect 200 + yield self.join(room=self.created_rmid, user=self.user_id) + (code, response) = yield self.mock_resource.trigger( + "PUT", + send_msg_path, + msg_content + ) + self.assertEquals(200, code, msg=str(response)) + + # send message in created room and left, expect 403 + yield self.leave(room=self.created_rmid, user=self.user_id) + (code, response) = yield self.mock_resource.trigger( + "PUT", + send_msg_path, + msg_content + ) + self.assertEquals(403, code, msg=str(response)) + + @defer.inlineCallbacks + def test_topic_perms(self): + topic_content = '{"topic":"My Topic Name"}' + topic_path = "/rooms/%s/state/m.room.topic" % self.created_rmid + + # set/get topic in uncreated room, expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid, + topic_content) + self.assertEquals(403, code, msg=str(response)) + (code, response) = yield self.mock_resource.trigger_get( + "/rooms/%s/state/m.room.topic" % self.uncreated_rmid) + self.assertEquals(403, code, msg=str(response)) + + # set/get topic in created PRIVATE room not joined, expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", topic_path, topic_content) + self.assertEquals(403, code, msg=str(response)) + (code, response) = yield self.mock_resource.trigger_get(topic_path) + self.assertEquals(403, code, msg=str(response)) + + # set topic in created PRIVATE room and invited, expect 403 + yield self.invite(room=self.created_rmid, src=self.rmcreator_id, + targ=self.user_id) + (code, response) = yield self.mock_resource.trigger( + "PUT", topic_path, topic_content) + self.assertEquals(403, code, msg=str(response)) + + # get topic in created PRIVATE room and invited, expect 403 + (code, response) = yield self.mock_resource.trigger_get(topic_path) + self.assertEquals(403, code, msg=str(response)) + + # set/get topic in created PRIVATE room and joined, expect 200 + yield self.join(room=self.created_rmid, user=self.user_id) + + # Only room ops can set topic by default + self.auth_user_id = self.rmcreator_id + (code, response) = yield self.mock_resource.trigger( + "PUT", topic_path, topic_content) + self.assertEquals(200, code, msg=str(response)) + self.auth_user_id = self.user_id + + (code, response) = yield self.mock_resource.trigger_get(topic_path) + self.assertEquals(200, code, msg=str(response)) + self.assert_dict(json.loads(topic_content), response) + + # set/get topic in created PRIVATE room and left, expect 403 + yield self.leave(room=self.created_rmid, user=self.user_id) + (code, response) = yield self.mock_resource.trigger( + "PUT", topic_path, topic_content) + self.assertEquals(403, code, msg=str(response)) + (code, response) = yield self.mock_resource.trigger_get(topic_path) + self.assertEquals(403, code, msg=str(response)) + + # get topic in PUBLIC room, not joined, expect 403 + (code, response) = yield self.mock_resource.trigger_get( + "/rooms/%s/state/m.room.topic" % self.created_public_rmid) + self.assertEquals(403, code, msg=str(response)) + + # set topic in PUBLIC room, not joined, expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", + "/rooms/%s/state/m.room.topic" % self.created_public_rmid, + topic_content) + self.assertEquals(403, code, msg=str(response)) + + @defer.inlineCallbacks + def _test_get_membership(self, room=None, members=[], expect_code=None): + path = "/rooms/%s/state/m.room.member/%s" + for member in members: + (code, response) = yield self.mock_resource.trigger_get( + path % + (room, member)) + self.assertEquals(expect_code, code) + + @defer.inlineCallbacks + def test_membership_basic_room_perms(self): + # === room does not exist === + room = self.uncreated_rmid + # get membership of self, get membership of other, uncreated room + # expect all 403s + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + # trying to invite people to this room should 403 + yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id, + expect_code=403) + + # set [invite/join/left] of self, set [invite/join/left] of other, + # expect all 403s + for usr in [self.user_id, self.rmcreator_id]: + yield self.join(room=room, user=usr, expect_code=404) + yield self.leave(room=room, user=usr, expect_code=403) + + @defer.inlineCallbacks + def test_membership_private_room_perms(self): + room = self.created_rmid + # get membership of self, get membership of other, private room + invite + # expect all 403s + yield self.invite(room=room, src=self.rmcreator_id, + targ=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + # get membership of self, get membership of other, private room + joined + # expect all 200s + yield self.join(room=room, user=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=200) + + # get membership of self, get membership of other, private room + left + # expect all 403s + yield self.leave(room=room, user=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + @defer.inlineCallbacks + def test_membership_public_room_perms(self): + room = self.created_public_rmid + # get membership of self, get membership of other, public room + invite + # expect 403 + yield self.invite(room=room, src=self.rmcreator_id, + targ=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + # get membership of self, get membership of other, public room + joined + # expect all 200s + yield self.join(room=room, user=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=200) + + # get membership of self, get membership of other, public room + left + # expect 403. + yield self.leave(room=room, user=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + @defer.inlineCallbacks + def test_invited_permissions(self): + room = self.created_rmid + yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) + + # set [invite/join/left] of other user, expect 403s + yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id, + expect_code=403) + yield self.change_membership(room=room, src=self.user_id, + targ=self.rmcreator_id, + membership=Membership.JOIN, + expect_code=403) + yield self.change_membership(room=room, src=self.user_id, + targ=self.rmcreator_id, + membership=Membership.LEAVE, + expect_code=403) + + @defer.inlineCallbacks + def test_joined_permissions(self): + room = self.created_rmid + yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) + yield self.join(room=room, user=self.user_id) + + # set invited of self, expect 403 + yield self.invite(room=room, src=self.user_id, targ=self.user_id, + expect_code=403) + + # set joined of self, expect 200 (NOOP) + yield self.join(room=room, user=self.user_id) + + other = "@burgundy:red" + # set invited of other, expect 200 + yield self.invite(room=room, src=self.user_id, targ=other, + expect_code=200) + + # set joined of other, expect 403 + yield self.change_membership(room=room, src=self.user_id, + targ=other, + membership=Membership.JOIN, + expect_code=403) + + # set left of other, expect 403 + yield self.change_membership(room=room, src=self.user_id, + targ=other, + membership=Membership.LEAVE, + expect_code=403) + + # set left of self, expect 200 + yield self.leave(room=room, user=self.user_id) + + @defer.inlineCallbacks + def test_leave_permissions(self): + room = self.created_rmid + yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) + yield self.join(room=room, user=self.user_id) + yield self.leave(room=room, user=self.user_id) + + # set [invite/join/left] of self, set [invite/join/left] of other, + # expect all 403s + for usr in [self.user_id, self.rmcreator_id]: + yield self.change_membership( + room=room, + src=self.user_id, + targ=usr, + membership=Membership.INVITE, + expect_code=403 + ) + + yield self.change_membership( + room=room, + src=self.user_id, + targ=usr, + membership=Membership.JOIN, + expect_code=403 + ) + + # It is always valid to LEAVE if you've already left (currently.) + yield self.change_membership( + room=room, + src=self.user_id, + targ=self.rmcreator_id, + membership=Membership.LEAVE, + expect_code=403 + ) + + +class RoomsMemberListTestCase(RestTestCase): + """ Tests /rooms/$room_id/members/list REST events.""" + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + self.auth_user_id = self.user_id + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + def tearDown(self): + pass + + @defer.inlineCallbacks + def test_get_member_list(self): + 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)) + + @defer.inlineCallbacks + def test_get_member_list_no_room(self): + (code, response) = yield self.mock_resource.trigger_get( + "/rooms/roomdoesnotexist/members") + self.assertEquals(403, code, msg=str(response)) + + @defer.inlineCallbacks + def test_get_member_list_no_permission(self): + 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_creator = "@some_other_guy:red" + room_id = yield self.create_room_as(room_creator) + room_path = "/rooms/%s/members" % room_id + yield self.invite(room=room_id, src=room_creator, + targ=self.user_id) + # can't see list if you're just invited. + (code, response) = yield self.mock_resource.trigger_get(room_path) + self.assertEquals(403, code, msg=str(response)) + + yield self.join(room=room_id, user=self.user_id) + # can see list now joined + (code, response) = yield self.mock_resource.trigger_get(room_path) + self.assertEquals(200, code, msg=str(response)) + + yield self.leave(room=room_id, user=self.user_id) + # can no longer see list, you've left. + (code, response) = yield self.mock_resource.trigger_get(room_path) + self.assertEquals(403, code, msg=str(response)) + + +class RoomsCreateTestCase(RestTestCase): + """ Tests /rooms and /rooms/$room_id REST events. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + def tearDown(self): + pass + + @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", + "/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", + "/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", + "/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", + "/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", + "/createRoom", + '{"visibili') + self.assertEquals(400, code) + + (code, response) = yield self.mock_resource.trigger( + "POST", + "/createRoom", + '["hello"]') + self.assertEquals(400, code) + + +class RoomTopicTestCase(RestTestCase): + """ Tests /rooms/$room_id/topic REST events. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + # create the room + 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 + + @defer.inlineCallbacks + def test_invalid_puts(self): + # missing keys or invalid json + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '{}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '{"_name":"bob"}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '{"nao') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '[{"_name":"bob"},{"_name":"jill"}]') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, 'text only') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '') + self.assertEquals(400, code, msg=str(response)) + + # valid key, wrong type + content = '{"topic":["Topic name"]}' + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, content) + self.assertEquals(400, code, msg=str(response)) + + @defer.inlineCallbacks + def test_rooms_topic(self): + # nothing should be there + (code, response) = yield self.mock_resource.trigger_get(self.path) + self.assertEquals(404, code, msg=str(response)) + + # valid put + content = '{"topic":"Topic name"}' + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, content) + self.assertEquals(200, code, msg=str(response)) + + # valid get + (code, response) = yield self.mock_resource.trigger_get(self.path) + self.assertEquals(200, code, msg=str(response)) + self.assert_dict(json.loads(content), response) + + @defer.inlineCallbacks + def test_rooms_topic_with_extra_keys(self): + # valid put with extra keys + content = '{"topic":"Seasons","subtopic":"Summer"}' + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, content) + self.assertEquals(200, code, msg=str(response)) + + # valid get + (code, response) = yield self.mock_resource.trigger_get(self.path) + self.assertEquals(200, code, msg=str(response)) + self.assert_dict(json.loads(content), response) + + +class RoomMemberStateTestCase(RestTestCase): + """ Tests /rooms/$room_id/members/$user_id/state REST events. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + self.room_id = yield self.create_room_as(self.user_id) + + def tearDown(self): + pass + + @defer.inlineCallbacks + def test_invalid_puts(self): + path = "/rooms/%s/state/m.room.member/%s" % (self.room_id, self.user_id) + # missing keys or invalid json + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{"_name":"bob"}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{"nao') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '[{"_name":"bob"},{"_name":"jill"}]') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, 'text only') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '') + self.assertEquals(400, code, msg=str(response)) + + # valid keys, wrong types + content = ('{"membership":["%s","%s","%s"]}' % + (Membership.INVITE, Membership.JOIN, Membership.LEAVE)) + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(400, code, msg=str(response)) + + @defer.inlineCallbacks + def test_rooms_members_self(self): + path = "/rooms/%s/state/m.room.member/%s" % ( + urllib.quote(self.room_id), self.user_id + ) + + # valid join message (NOOP since we made the room) + content = '{"membership":"%s"}' % Membership.JOIN + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("GET", path, None) + self.assertEquals(200, code, msg=str(response)) + + expected_response = { + "membership": Membership.JOIN, + } + self.assertEquals(expected_response, response) + + @defer.inlineCallbacks + def test_rooms_members_other(self): + self.other_id = "@zzsid1:red" + path = "/rooms/%s/state/m.room.member/%s" % ( + urllib.quote(self.room_id), self.other_id + ) + + # valid invite message + content = '{"membership":"%s"}' % Membership.INVITE + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("GET", path, None) + self.assertEquals(200, code, msg=str(response)) + self.assertEquals(json.loads(content), response) + + @defer.inlineCallbacks + def test_rooms_members_other_custom_keys(self): + self.other_id = "@zzsid1:red" + path = "/rooms/%s/state/m.room.member/%s" % ( + urllib.quote(self.room_id), self.other_id + ) + + # valid invite message with custom key + content = ('{"membership":"%s","invite_text":"%s"}' % + (Membership.INVITE, "Join us!")) + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("GET", path, None) + self.assertEquals(200, code, msg=str(response)) + self.assertEquals(json.loads(content), response) + + +class RoomMessagesTestCase(RestTestCase): + """ Tests /rooms/$room_id/messages/$user_id/$msg_id REST events. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + self.room_id = yield self.create_room_as(self.user_id) + + def tearDown(self): + pass + + @defer.inlineCallbacks + def test_invalid_puts(self): + path = "/rooms/%s/send/m.room.message/mid1" % ( + urllib.quote(self.room_id)) + # missing keys or invalid json + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{"_name":"bob"}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{"nao') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '[{"_name":"bob"},{"_name":"jill"}]') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, 'text only') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '') + self.assertEquals(400, code, msg=str(response)) + + @defer.inlineCallbacks + def test_rooms_messages_sent(self): + path = "/rooms/%s/send/m.room.message/mid1" % ( + urllib.quote(self.room_id)) + + content = '{"body":"test","msgtype":{"type":"a"}}' + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(400, code, msg=str(response)) + + # custom message types + content = '{"body":"test","msgtype":"test.custom.text"}' + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + +# (code, response) = yield self.mock_resource.trigger("GET", path, None) +# self.assertEquals(200, code, msg=str(response)) +# self.assert_dict(json.loads(content), response) + + # m.text message type + path = "/rooms/%s/send/m.room.message/mid2" % ( + urllib.quote(self.room_id)) + content = '{"body":"test2","msgtype":"m.text"}' + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + + +class RoomInitialSyncTestCase(RestTestCase): + """ Tests /rooms/$room_id/initialSync. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + # Since I'm getting my own presence I need to exist as far as presence + # is concerned. + hs.get_handlers().presence_handler.registered_user( + hs.parse_userid(self.user_id) + ) + + # create the room + self.room_id = yield self.create_room_as(self.user_id) + + @defer.inlineCallbacks + def test_initial_sync(self): + (code, response) = yield self.mock_resource.trigger_get( + "/rooms/%s/initialSync" % self.room_id) + self.assertEquals(200, code) + + self.assertEquals(self.room_id, response["room_id"]) + self.assertEquals("join", response["membership"]) + + # Room state is easier to assert on if we unpack it into a dict + state = {} + for event in response["state"]: + if "state_key" not in event: + continue + t = event["type"] + if t not in state: + state[t] = [] + state[t].append(event) + + self.assertTrue("m.room.create" in state) + + self.assertTrue("messages" in response) + self.assertTrue("chunk" in response["messages"]) + self.assertTrue("end" in response["messages"]) + + self.assertTrue("presence" in response) + + presence_by_user = {e["content"]["user_id"]: e + for e in response["presence"] + } + self.assertTrue(self.user_id in presence_by_user) + self.assertEquals("m.presence", presence_by_user[self.user_id]["type"]) diff --git a/tests/client/v1/test_typing.py b/tests/client/v1/test_typing.py new file mode 100644 index 0000000000..d6d677bde3 --- /dev/null +++ b/tests/client/v1/test_typing.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests REST events for /rooms paths.""" + +# twisted imports +from twisted.internet import defer + +import synapse.client.v1.room +from synapse.server import HomeServer + +from ...utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey +from .utils import RestTestCase + +from mock import Mock, NonCallableMock + + +PATH_PREFIX = "/_matrix/client/api/v1" + + +class RoomTypingTestCase(RestTestCase): + """ Tests /rooms/$room_id/typing/$user_id REST API. """ + user_id = "@sid:red" + + @defer.inlineCallbacks + def setUp(self): + self.clock = MockClock() + + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + clock=self.clock, + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.hs = hs + + self.event_source = hs.get_event_sources().sources["typing"] + + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + def get_room_members(room_id): + if room_id == self.room_id: + return defer.succeed([hs.parse_userid(self.user_id)]) + else: + return defer.succeed([]) + + @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 hs.is_mine(member): + if localusers is not None: + localusers.add(member) + else: + if remotedomains is not None: + remotedomains.add(member.domain) + hs.get_handlers().room_member_handler.fetch_room_distributions_into = ( + fetch_room_distributions_into) + + synapse.client.v1.room.register_servlets(hs, self.mock_resource) + + self.room_id = yield self.create_room_as(self.user_id) + # Need another user to make notifications actually work + yield self.join(self.room_id, user="@jim:red") + + def tearDown(self): + self.hs.get_handlers().typing_notification_handler.tearDown() + + @defer.inlineCallbacks + def test_set_typing(self): + (code, _) = yield self.mock_resource.trigger("PUT", + "/rooms/%s/typing/%s" % (self.room_id, self.user_id), + '{"typing": true, "timeout": 30000}' + ) + self.assertEquals(200, code) + + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals( + self.event_source.get_new_events_for_user(self.user_id, 0, None)[0], + [ + {"type": "m.typing", + "room_id": self.room_id, + "content": { + "user_ids": [self.user_id], + }}, + ] + ) + + @defer.inlineCallbacks + def test_set_not_typing(self): + (code, _) = yield self.mock_resource.trigger("PUT", + "/rooms/%s/typing/%s" % (self.room_id, self.user_id), + '{"typing": false}' + ) + self.assertEquals(200, code) + + @defer.inlineCallbacks + def test_typing_timeout(self): + (code, _) = yield self.mock_resource.trigger("PUT", + "/rooms/%s/typing/%s" % (self.room_id, self.user_id), + '{"typing": true, "timeout": 30000}' + ) + self.assertEquals(200, code) + + self.assertEquals(self.event_source.get_current_key(), 1) + + self.clock.advance_time(31); + + self.assertEquals(self.event_source.get_current_key(), 2) + + (code, _) = yield self.mock_resource.trigger("PUT", + "/rooms/%s/typing/%s" % (self.room_id, self.user_id), + '{"typing": true, "timeout": 30000}' + ) + self.assertEquals(200, code) + + self.assertEquals(self.event_source.get_current_key(), 3) diff --git a/tests/client/v1/utils.py b/tests/client/v1/utils.py new file mode 100644 index 0000000000..579441fb4a --- /dev/null +++ b/tests/client/v1/utils.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# twisted imports +from twisted.internet import defer + +# trial imports +from tests import unittest + +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. + + This subclass assumes there are mock_resource and auth_user_id attributes. + """ + + def __init__(self, *args, **kwargs): + super(RestTestCase, self).__init__(*args, **kwargs) + self.mock_resource = None + self.auth_user_id = None + + def mock_get_user_by_token(self, token=None): + return self.auth_user_id + + @defer.inlineCallbacks + 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 = "/createRoom" + content = "{}" + if not is_public: + content = '{"visibility":"private"}' + if tok: + path = path + "?access_token=%s" % tok + (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): + yield self.change_membership(room=room, src=src, targ=targ, tok=tok, + membership=Membership.INVITE, + expect_code=expect_code) + + @defer.inlineCallbacks + def join(self, room=None, user=None, expect_code=200, tok=None): + yield self.change_membership(room=room, src=user, targ=user, tok=tok, + membership=Membership.JOIN, + expect_code=expect_code) + + @defer.inlineCallbacks + def leave(self, room=None, user=None, expect_code=200, tok=None): + yield self.change_membership(room=room, src=user, targ=user, tok=tok, + membership=Membership.LEAVE, + expect_code=expect_code) + + @defer.inlineCallbacks + def change_membership(self, room, src, targ, membership, tok=None, + expect_code=200): + temp_id = self.auth_user_id + self.auth_user_id = src + + path = "/rooms/%s/state/m.room.member/%s" % (room, targ) + if tok: + path = path + "?access_token=%s" % tok + + data = { + "membership": membership + } + + (code, response) = yield self.mock_resource.trigger("PUT", path, + json.dumps(data)) + self.assertEquals(expect_code, code, msg=str(response)) + + self.auth_user_id = temp_id + + @defer.inlineCallbacks + def register(self, user_id): + (code, response) = yield self.mock_resource.trigger( + "POST", + "/register", + json.dumps({ + "user": user_id, + "password": "test", + "type": "m.login.password" + })) + self.assertEquals(200, code) + defer.returnValue(response) + + @defer.inlineCallbacks + def send(self, room_id, body=None, txn_id=None, tok=None, + expect_code=200): + if txn_id is None: + txn_id = "m%s" % (str(time.time())) + if body is None: + body = "body_text_here" + + path = "/rooms/%s/send/m.room.message/%s" % (room_id, txn_id) + content = '{"msgtype":"m.text","body":"%s"}' % body + if tok: + path = path + "?access_token=%s" % tok + + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(expect_code, code, msg=str(response)) + + def assert_dict(self, required, actual): + """Does a partial assert of a dict. + + Args: + required (dict): The keys and value which MUST be in 'actual'. + actual (dict): The test result. Extra keys will not be checked. + """ + for key in required: + self.assertEquals(required[key], actual[key], + msg="%s mismatch. %s" % (key, actual)) diff --git a/tests/rest/__init__.py b/tests/rest/__init__.py deleted file mode 100644 index 9bff9ec169..0000000000 --- a/tests/rest/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - diff --git a/tests/rest/test_events.py b/tests/rest/test_events.py deleted file mode 100644 index d3159e2cf4..0000000000 --- a/tests/rest/test_events.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" Tests REST events for /events paths.""" -from tests import unittest - -# twisted imports -from twisted.internet import defer - -import synapse.rest.events -import synapse.rest.register -import synapse.rest.room - -from synapse.server import HomeServer - -from ..utils import MockHttpResource, SQLiteMemoryDbPool, MockKey -from .utils import RestTestCase - -from mock import Mock, NonCallableMock - - -PATH_PREFIX = "/_matrix/client/api/v1" - - -class EventStreamPaginationApiTestCase(unittest.TestCase): - """ Tests event streaming query parameters and start/end keys used in the - Pagination stream API. """ - user_id = "sid1" - - def setUp(self): - # configure stream and inject items - pass - - def tearDown(self): - pass - - def TODO_test_long_poll(self): - # stream from 'end' key, send (self+other) message, expect message. - - # stream from 'END', send (self+other) message, expect message. - - # stream from 'end' key, send (self+other) topic, expect topic. - - # stream from 'END', send (self+other) topic, expect topic. - - # stream from 'end' key, send (self+other) invite, expect invite. - - # stream from 'END', send (self+other) invite, expect invite. - - pass - - def TODO_test_stream_forward(self): - # stream from START, expect injected items - - # stream from 'start' key, expect same content - - # stream from 'end' key, expect nothing - - # stream from 'END', expect nothing - - # The following is needed for cases where content is removed e.g. you - # left a room, so the token you're streaming from is > the one that - # would be returned naturally from START>END. - # stream from very new token (higher than end key), expect same token - # returned as end key - pass - - def TODO_test_limits(self): - # stream from a key, expect limit_num items - - # stream from START, expect limit_num items - - pass - - def TODO_test_range(self): - # stream from key to key, expect X items - - # stream from key to END, expect X items - - # stream from START to key, expect X items - - # stream from START to END, expect all items - pass - - def TODO_test_direction(self): - # stream from END to START and fwds, expect newest first - - # stream from END to START and bwds, expect oldest first - - # stream from START to END and fwds, expect oldest first - - # stream from START to END and bwds, expect newest first - - pass - - -class EventStreamPermissionsTestCase(RestTestCase): - """ Tests event streaming (GET /events). """ - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "test", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - clock=Mock(spec=[ - "call_later", - "cancel_call_later", - "time_msec", - "time" - ]), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - hs.config.enable_registration_captcha = False - - hs.get_handlers().federation_handler = Mock() - - hs.get_clock().time_msec.return_value = 1000000 - hs.get_clock().time.return_value = 1000 - - synapse.rest.register.register_servlets(hs, self.mock_resource) - synapse.rest.events.register_servlets(hs, self.mock_resource) - synapse.rest.room.register_servlets(hs, self.mock_resource) - - # register an account - self.user_id = "sid1" - response = yield self.register(self.user_id) - self.token = response["access_token"] - self.user_id = response["user_id"] - - # register a 2nd account - self.other_user = "other1" - response = yield self.register(self.other_user) - self.other_token = response["access_token"] - self.other_user = response["user_id"] - - def tearDown(self): - pass - - @defer.inlineCallbacks - def test_stream_basic_permissions(self): - # invalid token, expect 403 - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s" % ("invalid" + self.token, ) - ) - self.assertEquals(403, code, msg=str(response)) - - # valid token, expect content - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s&timeout=0" % (self.token,) - ) - self.assertEquals(200, code, msg=str(response)) - self.assertTrue("chunk" in response) - self.assertTrue("start" in response) - self.assertTrue("end" in response) - - @defer.inlineCallbacks - def test_stream_room_permissions(self): - 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) - yield self.invite( - room_id, - src=self.other_user, - targ=self.user_id, - tok=self.other_token - ) - - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s&timeout=0" % (self.token,) - ) - self.assertEquals(200, code, msg=str(response)) - - self.assertEquals(0, len(response["chunk"])) - - # joined room (expect all content for room) - yield self.join(room=room_id, user=self.user_id, tok=self.token) - - # left to room (expect no content for room) - - def TODO_test_stream_items(self): - # new user, no content - - # join room, expect 1 item (join) - - # send message, expect 2 items (join,send) - - # set topic, expect 3 items (join,send,topic) - - # someone else join room, expect 4 (join,send,topic,join) - - # someone else send message, expect 5 (join,send.topic,join,send) - - # someone else set topic, expect 6 (join,send,topic,join,send,topic) - pass diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py deleted file mode 100644 index 769c7824bc..0000000000 --- a/tests/rest/test_presence.py +++ /dev/null @@ -1,372 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /presence paths.""" - -from tests import unittest -from twisted.internet import defer - -from mock import Mock - -from ..utils import MockHttpResource, MockKey - -from synapse.api.constants import PresenceState -from synapse.handlers.presence import PresenceHandler -from synapse.server import HomeServer - - -OFFLINE = PresenceState.OFFLINE -UNAVAILABLE = PresenceState.UNAVAILABLE -ONLINE = PresenceState.ONLINE - - -myid = "@apple:test" -PATH_PREFIX = "/_matrix/client/api/v1" - - -class JustPresenceHandlers(object): - def __init__(self, hs): - self.presence_handler = PresenceHandler(hs) - - -class PresenceStateTestCase(unittest.TestCase): - - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.mock_config = Mock() - self.mock_config.signing_key = [MockKey()] - hs = HomeServer("test", - db_pool=None, - datastore=Mock(spec=[ - "get_presence_state", - "set_presence_state", - "insert_client_ip", - ]), - http_client=None, - resource_for_client=self.mock_resource, - resource_for_federation=self.mock_resource, - config=self.mock_config, - ) - hs.handlers = JustPresenceHandlers(hs) - - self.datastore = hs.get_datastore() - - def get_presence_list(*a, **kw): - return defer.succeed([]) - self.datastore.get_presence_list = get_presence_list - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(myid), - "admin": False, - "device_id": None, - } - - hs.get_auth().get_user_by_token = _get_user_by_token - - room_member_handler = hs.handlers.room_member_handler = Mock( - spec=[ - "get_rooms_for_user", - ] - ) - - def get_rooms_for_user(user): - return defer.succeed([]) - room_member_handler.get_rooms_for_user = get_rooms_for_user - - hs.register_servlets() - - self.u_apple = hs.parse_userid(myid) - - @defer.inlineCallbacks - def test_get_my_status(self): - mocked_get = self.datastore.get_presence_state - mocked_get.return_value = defer.succeed( - {"state": ONLINE, "status_msg": "Available"} - ) - - (code, response) = yield self.mock_resource.trigger("GET", - "/presence/%s/status" % (myid), None) - - self.assertEquals(200, code) - self.assertEquals( - {"presence": ONLINE, "status_msg": "Available"}, - response - ) - mocked_get.assert_called_with("apple") - - @defer.inlineCallbacks - def test_set_my_status(self): - mocked_set = self.datastore.set_presence_state - mocked_set.return_value = defer.succeed({"state": OFFLINE}) - - (code, response) = yield self.mock_resource.trigger("PUT", - "/presence/%s/status" % (myid), - '{"presence": "unavailable", "status_msg": "Away"}') - - self.assertEquals(200, code) - mocked_set.assert_called_with("apple", - {"state": UNAVAILABLE, "status_msg": "Away"} - ) - - -class PresenceListTestCase(unittest.TestCase): - - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.mock_config = Mock() - self.mock_config.signing_key = [MockKey()] - - hs = HomeServer("test", - db_pool=None, - datastore=Mock(spec=[ - "has_presence_state", - "get_presence_state", - "allow_presence_visible", - "is_presence_visible", - "add_presence_list_pending", - "set_presence_list_accepted", - "del_presence_list", - "get_presence_list", - "insert_client_ip", - ]), - http_client=None, - resource_for_client=self.mock_resource, - resource_for_federation=self.mock_resource, - config=self.mock_config, - ) - hs.handlers = JustPresenceHandlers(hs) - - self.datastore = hs.get_datastore() - - def has_presence_state(user_localpart): - return defer.succeed( - user_localpart in ("apple", "banana",) - ) - self.datastore.has_presence_state = has_presence_state - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(myid), - "admin": False, - "device_id": None, - } - - room_member_handler = hs.handlers.room_member_handler = Mock( - spec=[ - "get_rooms_for_user", - ] - ) - - hs.get_auth().get_user_by_token = _get_user_by_token - - hs.register_servlets() - - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - - @defer.inlineCallbacks - def test_get_my_list(self): - self.datastore.get_presence_list.return_value = defer.succeed( - [{"observed_user_id": "@banana:test"}], - ) - - (code, response) = yield self.mock_resource.trigger("GET", - "/presence/list/%s" % (myid), None) - - self.assertEquals(200, code) - self.assertEquals([ - {"user_id": "@banana:test", "presence": OFFLINE}, - ], response) - - self.datastore.get_presence_list.assert_called_with( - "apple", accepted=True - ) - - @defer.inlineCallbacks - def test_invite(self): - self.datastore.add_presence_list_pending.return_value = ( - defer.succeed(()) - ) - self.datastore.is_presence_visible.return_value = defer.succeed( - True - ) - - (code, response) = yield self.mock_resource.trigger("POST", - "/presence/list/%s" % (myid), - """{"invite": ["@banana:test"]}""" - ) - - self.assertEquals(200, code) - - self.datastore.add_presence_list_pending.assert_called_with( - "apple", "@banana:test" - ) - self.datastore.set_presence_list_accepted.assert_called_with( - "apple", "@banana:test" - ) - - @defer.inlineCallbacks - def test_drop(self): - self.datastore.del_presence_list.return_value = ( - defer.succeed(()) - ) - - (code, response) = yield self.mock_resource.trigger("POST", - "/presence/list/%s" % (myid), - """{"drop": ["@banana:test"]}""" - ) - - self.assertEquals(200, code) - - self.datastore.del_presence_list.assert_called_with( - "apple", "@banana:test" - ) - - -class PresenceEventStreamTestCase(unittest.TestCase): - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - self.mock_config = Mock() - self.mock_config.signing_key = [MockKey()] - - # HIDEOUS HACKERY - # TODO(paul): This should be injected in via the HomeServer DI system - from synapse.streams.events import ( - PresenceEventSource, NullSource, EventSources - ) - - old_SOURCE_TYPES = EventSources.SOURCE_TYPES - def tearDown(): - EventSources.SOURCE_TYPES = old_SOURCE_TYPES - self.tearDown = tearDown - - EventSources.SOURCE_TYPES = { - k: NullSource for k in old_SOURCE_TYPES.keys() - } - EventSources.SOURCE_TYPES["presence"] = PresenceEventSource - - hs = HomeServer("test", - db_pool=None, - http_client=None, - resource_for_client=self.mock_resource, - resource_for_federation=self.mock_resource, - datastore=Mock(spec=[ - "set_presence_state", - "get_presence_list", - ]), - clock=Mock(spec=[ - "call_later", - "cancel_call_later", - "time_msec", - ]), - config=self.mock_config, - ) - - hs.get_clock().time_msec.return_value = 1000000 - - def _get_user_by_req(req=None): - return hs.parse_userid(myid) - - hs.get_auth().get_user_by_req = _get_user_by_req - - hs.register_servlets() - - hs.handlers.room_member_handler = Mock(spec=[]) - - self.room_members = [] - - def get_rooms_for_user(user): - if user in self.room_members: - return ["a-room"] - else: - return [] - hs.handlers.room_member_handler.get_rooms_for_user = get_rooms_for_user - - self.mock_datastore = hs.get_datastore() - - def get_profile_displayname(user_id): - return defer.succeed("Frank") - self.mock_datastore.get_profile_displayname = get_profile_displayname - - def get_profile_avatar_url(user_id): - return defer.succeed(None) - self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url - - def user_rooms_intersect(user_list): - room_member_ids = map(lambda u: u.to_string(), self.room_members) - - shared = all(map(lambda i: i in room_member_ids, user_list)) - return defer.succeed(shared) - self.mock_datastore.user_rooms_intersect = user_rooms_intersect - - def get_joined_hosts_for_room(room_id): - return [] - self.mock_datastore.get_joined_hosts_for_room = get_joined_hosts_for_room - - self.presence = hs.get_handlers().presence_handler - - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - - @defer.inlineCallbacks - def test_shortpoll(self): - self.room_members = [self.u_apple, self.u_banana] - - self.mock_datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE} - ) - self.mock_datastore.get_presence_list.return_value = defer.succeed( - [] - ) - - (code, response) = yield self.mock_resource.trigger("GET", - "/events?timeout=0", None) - - self.assertEquals(200, code) - - # We've forced there to be only one data stream so the tokens will - # all be ours - - # I'll already get my own presence state change - self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []}, - response - ) - - self.mock_datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE} - ) - self.mock_datastore.get_presence_list.return_value = defer.succeed( - [] - ) - - yield self.presence.set_state(self.u_banana, self.u_banana, - state={"presence": ONLINE} - ) - - (code, response) = yield self.mock_resource.trigger("GET", - "/events?from=0_1_0&timeout=0", None) - - self.assertEquals(200, code) - self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [ - {"type": "m.presence", - "content": { - "user_id": "@banana:test", - "presence": ONLINE, - "displayname": "Frank", - "last_active_ago": 0, - }}, - ]}, response) diff --git a/tests/rest/test_profile.py b/tests/rest/test_profile.py deleted file mode 100644 index 3a0d1e700a..0000000000 --- a/tests/rest/test_profile.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /profile paths.""" - -from tests import unittest -from twisted.internet import defer - -from mock import Mock, NonCallableMock - -from ..utils import MockHttpResource, MockKey - -from synapse.api.errors import SynapseError, AuthError -from synapse.server import HomeServer - -myid = "@1234ABCD:test" -PATH_PREFIX = "/_matrix/client/api/v1" - - -class ProfileTestCase(unittest.TestCase): - """ Tests profile management. """ - - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.mock_handler = Mock(spec=[ - "get_displayname", - "set_displayname", - "get_avatar_url", - "set_avatar_url", - ]) - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - hs = HomeServer("test", - db_pool=None, - http_client=None, - resource_for_client=self.mock_resource, - federation=Mock(), - replication_layer=Mock(), - datastore=None, - config=self.mock_config, - ) - - def _get_user_by_req(request=None): - return hs.parse_userid(myid) - - hs.get_auth().get_user_by_req = _get_user_by_req - - hs.get_handlers().profile_handler = self.mock_handler - - hs.register_servlets() - - @defer.inlineCallbacks - def test_get_my_name(self): - mocked_get = self.mock_handler.get_displayname - mocked_get.return_value = defer.succeed("Frank") - - (code, response) = yield self.mock_resource.trigger("GET", - "/profile/%s/displayname" % (myid), None) - - self.assertEquals(200, code) - self.assertEquals({"displayname": "Frank"}, response) - self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD") - - @defer.inlineCallbacks - def test_set_my_name(self): - mocked_set = self.mock_handler.set_displayname - mocked_set.return_value = defer.succeed(()) - - (code, response) = yield self.mock_resource.trigger("PUT", - "/profile/%s/displayname" % (myid), - '{"displayname": "Frank Jr."}') - - self.assertEquals(200, code) - self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD") - self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD") - self.assertEquals(mocked_set.call_args[0][2], "Frank Jr.") - - @defer.inlineCallbacks - def test_set_my_name_noauth(self): - mocked_set = self.mock_handler.set_displayname - mocked_set.side_effect = AuthError(400, "message") - - (code, response) = yield self.mock_resource.trigger("PUT", - "/profile/%s/displayname" % ("@4567:test"), '"Frank Jr."') - - self.assertTrue(400 <= code < 499, - msg="code %d is in the 4xx range" % (code)) - - @defer.inlineCallbacks - def test_get_other_name(self): - mocked_get = self.mock_handler.get_displayname - mocked_get.return_value = defer.succeed("Bob") - - (code, response) = yield self.mock_resource.trigger("GET", - "/profile/%s/displayname" % ("@opaque:elsewhere"), None) - - self.assertEquals(200, code) - self.assertEquals({"displayname": "Bob"}, response) - - @defer.inlineCallbacks - def test_set_other_name(self): - mocked_set = self.mock_handler.set_displayname - mocked_set.side_effect = SynapseError(400, "message") - - (code, response) = yield self.mock_resource.trigger("PUT", - "/profile/%s/displayname" % ("@opaque:elsewhere"), None) - - self.assertTrue(400 <= code <= 499, - msg="code %d is in the 4xx range" % (code)) - - @defer.inlineCallbacks - def test_get_my_avatar(self): - mocked_get = self.mock_handler.get_avatar_url - mocked_get.return_value = defer.succeed("http://my.server/me.png") - - (code, response) = yield self.mock_resource.trigger("GET", - "/profile/%s/avatar_url" % (myid), None) - - self.assertEquals(200, code) - self.assertEquals({"avatar_url": "http://my.server/me.png"}, response) - self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD") - - @defer.inlineCallbacks - def test_set_my_avatar(self): - mocked_set = self.mock_handler.set_avatar_url - mocked_set.return_value = defer.succeed(()) - - (code, response) = yield self.mock_resource.trigger("PUT", - "/profile/%s/avatar_url" % (myid), - '{"avatar_url": "http://my.server/pic.gif"}') - - self.assertEquals(200, code) - self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD") - self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD") - self.assertEquals(mocked_set.call_args[0][2], - "http://my.server/pic.gif") diff --git a/tests/rest/test_rooms.py b/tests/rest/test_rooms.py deleted file mode 100644 index 8e65ff9a1c..0000000000 --- a/tests/rest/test_rooms.py +++ /dev/null @@ -1,1068 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /rooms paths.""" - -# twisted imports -from twisted.internet import defer - -import synapse.rest.room -from synapse.api.constants import Membership - -from synapse.server import HomeServer - -from tests import unittest - -# python imports -import json -import urllib -import types - -from ..utils import MockHttpResource, SQLiteMemoryDbPool, MockKey -from .utils import RestTestCase - -from mock import Mock, NonCallableMock - -PATH_PREFIX = "/_matrix/client/api/v1" - - -class RoomPermissionsTestCase(RestTestCase): - """ Tests room permissions. """ - user_id = "@sid1:red" - rmcreator_id = "@notme:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - self.auth_user_id = self.rmcreator_id - - synapse.rest.room.register_servlets(hs, self.mock_resource) - - self.auth = hs.get_auth() - - # create some rooms under the name rmcreator_id - self.uncreated_rmid = "!aa:test" - - self.created_rmid = yield self.create_room_as(self.rmcreator_id, - is_public=False) - - 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" % - (self.created_rmid)) - (code, response) = yield self.mock_resource.trigger( - "PUT", - self.created_rmid_msg_path, - '{"msgtype":"m.text","body":"test msg"}') - self.assertEquals(200, code, msg=str(response)) - - # set topic for public room - (code, response) = yield self.mock_resource.trigger( - "PUT", - "/rooms/%s/state/m.room.topic" % self.created_public_rmid, - '{"topic":"Public Room Topic"}') - self.assertEquals(200, code, msg=str(response)) - - # auth as user_id now - self.auth_user_id = self.user_id - - def tearDown(self): - pass - -# @defer.inlineCallbacks -# def test_get_message(self): -# # get message in uncreated room, expect 403 -# (code, response) = yield self.mock_resource.trigger_get( -# "/rooms/noroom/messages/someid/m1") -# self.assertEquals(403, code, msg=str(response)) -# -# # get message in created room not joined (no state), expect 403 -# (code, response) = yield self.mock_resource.trigger_get( -# self.created_rmid_msg_path) -# self.assertEquals(403, code, msg=str(response)) -# -# # get message in created room and invited, expect 403 -# yield self.invite(room=self.created_rmid, src=self.rmcreator_id, -# targ=self.user_id) -# (code, response) = yield self.mock_resource.trigger_get( -# self.created_rmid_msg_path) -# self.assertEquals(403, code, msg=str(response)) -# -# # get message in created room and joined, expect 200 -# yield self.join(room=self.created_rmid, user=self.user_id) -# (code, response) = yield self.mock_resource.trigger_get( -# self.created_rmid_msg_path) -# self.assertEquals(200, code, msg=str(response)) -# -# # get message in created room and left, expect 403 -# yield self.leave(room=self.created_rmid, user=self.user_id) -# (code, response) = yield self.mock_resource.trigger_get( -# self.created_rmid_msg_path) -# self.assertEquals(403, code, msg=str(response)) - - @defer.inlineCallbacks - def test_send_message(self): - msg_content = '{"msgtype":"m.text","body":"hello"}' - send_msg_path = ( - "/rooms/%s/send/m.room.message/mid1" % (self.created_rmid,) - ) - - # send message in uncreated room, expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", - "/rooms/%s/send/m.room.message/mid2" % (self.uncreated_rmid,), - msg_content - ) - self.assertEquals(403, code, msg=str(response)) - - # send message in created room not joined (no state), expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", - send_msg_path, - msg_content - ) - self.assertEquals(403, code, msg=str(response)) - - # send message in created room and invited, expect 403 - yield self.invite( - room=self.created_rmid, - src=self.rmcreator_id, - targ=self.user_id - ) - (code, response) = yield self.mock_resource.trigger( - "PUT", - send_msg_path, - msg_content - ) - self.assertEquals(403, code, msg=str(response)) - - # send message in created room and joined, expect 200 - yield self.join(room=self.created_rmid, user=self.user_id) - (code, response) = yield self.mock_resource.trigger( - "PUT", - send_msg_path, - msg_content - ) - self.assertEquals(200, code, msg=str(response)) - - # send message in created room and left, expect 403 - yield self.leave(room=self.created_rmid, user=self.user_id) - (code, response) = yield self.mock_resource.trigger( - "PUT", - send_msg_path, - msg_content - ) - self.assertEquals(403, code, msg=str(response)) - - @defer.inlineCallbacks - def test_topic_perms(self): - topic_content = '{"topic":"My Topic Name"}' - topic_path = "/rooms/%s/state/m.room.topic" % self.created_rmid - - # set/get topic in uncreated room, expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid, - topic_content) - self.assertEquals(403, code, msg=str(response)) - (code, response) = yield self.mock_resource.trigger_get( - "/rooms/%s/state/m.room.topic" % self.uncreated_rmid) - self.assertEquals(403, code, msg=str(response)) - - # set/get topic in created PRIVATE room not joined, expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", topic_path, topic_content) - self.assertEquals(403, code, msg=str(response)) - (code, response) = yield self.mock_resource.trigger_get(topic_path) - self.assertEquals(403, code, msg=str(response)) - - # set topic in created PRIVATE room and invited, expect 403 - yield self.invite(room=self.created_rmid, src=self.rmcreator_id, - targ=self.user_id) - (code, response) = yield self.mock_resource.trigger( - "PUT", topic_path, topic_content) - self.assertEquals(403, code, msg=str(response)) - - # get topic in created PRIVATE room and invited, expect 403 - (code, response) = yield self.mock_resource.trigger_get(topic_path) - self.assertEquals(403, code, msg=str(response)) - - # set/get topic in created PRIVATE room and joined, expect 200 - yield self.join(room=self.created_rmid, user=self.user_id) - - # Only room ops can set topic by default - self.auth_user_id = self.rmcreator_id - (code, response) = yield self.mock_resource.trigger( - "PUT", topic_path, topic_content) - self.assertEquals(200, code, msg=str(response)) - self.auth_user_id = self.user_id - - (code, response) = yield self.mock_resource.trigger_get(topic_path) - self.assertEquals(200, code, msg=str(response)) - self.assert_dict(json.loads(topic_content), response) - - # set/get topic in created PRIVATE room and left, expect 403 - yield self.leave(room=self.created_rmid, user=self.user_id) - (code, response) = yield self.mock_resource.trigger( - "PUT", topic_path, topic_content) - self.assertEquals(403, code, msg=str(response)) - (code, response) = yield self.mock_resource.trigger_get(topic_path) - self.assertEquals(403, code, msg=str(response)) - - # get topic in PUBLIC room, not joined, expect 403 - (code, response) = yield self.mock_resource.trigger_get( - "/rooms/%s/state/m.room.topic" % self.created_public_rmid) - self.assertEquals(403, code, msg=str(response)) - - # set topic in PUBLIC room, not joined, expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", - "/rooms/%s/state/m.room.topic" % self.created_public_rmid, - topic_content) - self.assertEquals(403, code, msg=str(response)) - - @defer.inlineCallbacks - def _test_get_membership(self, room=None, members=[], expect_code=None): - path = "/rooms/%s/state/m.room.member/%s" - for member in members: - (code, response) = yield self.mock_resource.trigger_get( - path % - (room, member)) - self.assertEquals(expect_code, code) - - @defer.inlineCallbacks - def test_membership_basic_room_perms(self): - # === room does not exist === - room = self.uncreated_rmid - # get membership of self, get membership of other, uncreated room - # expect all 403s - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - # trying to invite people to this room should 403 - yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id, - expect_code=403) - - # set [invite/join/left] of self, set [invite/join/left] of other, - # expect all 403s - for usr in [self.user_id, self.rmcreator_id]: - yield self.join(room=room, user=usr, expect_code=404) - yield self.leave(room=room, user=usr, expect_code=403) - - @defer.inlineCallbacks - def test_membership_private_room_perms(self): - room = self.created_rmid - # get membership of self, get membership of other, private room + invite - # expect all 403s - yield self.invite(room=room, src=self.rmcreator_id, - targ=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - # get membership of self, get membership of other, private room + joined - # expect all 200s - yield self.join(room=room, user=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=200) - - # get membership of self, get membership of other, private room + left - # expect all 403s - yield self.leave(room=room, user=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - @defer.inlineCallbacks - def test_membership_public_room_perms(self): - room = self.created_public_rmid - # get membership of self, get membership of other, public room + invite - # expect 403 - yield self.invite(room=room, src=self.rmcreator_id, - targ=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - # get membership of self, get membership of other, public room + joined - # expect all 200s - yield self.join(room=room, user=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=200) - - # get membership of self, get membership of other, public room + left - # expect 403. - yield self.leave(room=room, user=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - @defer.inlineCallbacks - def test_invited_permissions(self): - room = self.created_rmid - yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - - # set [invite/join/left] of other user, expect 403s - yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id, - expect_code=403) - yield self.change_membership(room=room, src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.JOIN, - expect_code=403) - yield self.change_membership(room=room, src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.LEAVE, - expect_code=403) - - @defer.inlineCallbacks - def test_joined_permissions(self): - room = self.created_rmid - yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - yield self.join(room=room, user=self.user_id) - - # set invited of self, expect 403 - yield self.invite(room=room, src=self.user_id, targ=self.user_id, - expect_code=403) - - # set joined of self, expect 200 (NOOP) - yield self.join(room=room, user=self.user_id) - - other = "@burgundy:red" - # set invited of other, expect 200 - yield self.invite(room=room, src=self.user_id, targ=other, - expect_code=200) - - # set joined of other, expect 403 - yield self.change_membership(room=room, src=self.user_id, - targ=other, - membership=Membership.JOIN, - expect_code=403) - - # set left of other, expect 403 - yield self.change_membership(room=room, src=self.user_id, - targ=other, - membership=Membership.LEAVE, - expect_code=403) - - # set left of self, expect 200 - yield self.leave(room=room, user=self.user_id) - - @defer.inlineCallbacks - def test_leave_permissions(self): - room = self.created_rmid - yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - yield self.join(room=room, user=self.user_id) - yield self.leave(room=room, user=self.user_id) - - # set [invite/join/left] of self, set [invite/join/left] of other, - # expect all 403s - for usr in [self.user_id, self.rmcreator_id]: - yield self.change_membership( - room=room, - src=self.user_id, - targ=usr, - membership=Membership.INVITE, - expect_code=403 - ) - - yield self.change_membership( - room=room, - src=self.user_id, - targ=usr, - membership=Membership.JOIN, - expect_code=403 - ) - - # It is always valid to LEAVE if you've already left (currently.) - yield self.change_membership( - room=room, - src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.LEAVE, - expect_code=403 - ) - - -class RoomsMemberListTestCase(RestTestCase): - """ Tests /rooms/$room_id/members/list REST events.""" - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - self.auth_user_id = self.user_id - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.room.register_servlets(hs, self.mock_resource) - - def tearDown(self): - pass - - @defer.inlineCallbacks - def test_get_member_list(self): - 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)) - - @defer.inlineCallbacks - def test_get_member_list_no_room(self): - (code, response) = yield self.mock_resource.trigger_get( - "/rooms/roomdoesnotexist/members") - self.assertEquals(403, code, msg=str(response)) - - @defer.inlineCallbacks - def test_get_member_list_no_permission(self): - 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_creator = "@some_other_guy:red" - room_id = yield self.create_room_as(room_creator) - room_path = "/rooms/%s/members" % room_id - yield self.invite(room=room_id, src=room_creator, - targ=self.user_id) - # can't see list if you're just invited. - (code, response) = yield self.mock_resource.trigger_get(room_path) - self.assertEquals(403, code, msg=str(response)) - - yield self.join(room=room_id, user=self.user_id) - # can see list now joined - (code, response) = yield self.mock_resource.trigger_get(room_path) - self.assertEquals(200, code, msg=str(response)) - - yield self.leave(room=room_id, user=self.user_id) - # can no longer see list, you've left. - (code, response) = yield self.mock_resource.trigger_get(room_path) - self.assertEquals(403, code, msg=str(response)) - - -class RoomsCreateTestCase(RestTestCase): - """ Tests /rooms and /rooms/$room_id REST events. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.room.register_servlets(hs, self.mock_resource) - - def tearDown(self): - pass - - @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", - "/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", - "/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", - "/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", - "/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", - "/createRoom", - '{"visibili') - self.assertEquals(400, code) - - (code, response) = yield self.mock_resource.trigger( - "POST", - "/createRoom", - '["hello"]') - self.assertEquals(400, code) - - -class RoomTopicTestCase(RestTestCase): - """ Tests /rooms/$room_id/topic REST events. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.room.register_servlets(hs, self.mock_resource) - - # create the room - 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 - - @defer.inlineCallbacks - def test_invalid_puts(self): - # missing keys or invalid json - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '{}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '{"_name":"bob"}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '{"nao') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '[{"_name":"bob"},{"_name":"jill"}]') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, 'text only') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '') - self.assertEquals(400, code, msg=str(response)) - - # valid key, wrong type - content = '{"topic":["Topic name"]}' - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, content) - self.assertEquals(400, code, msg=str(response)) - - @defer.inlineCallbacks - def test_rooms_topic(self): - # nothing should be there - (code, response) = yield self.mock_resource.trigger_get(self.path) - self.assertEquals(404, code, msg=str(response)) - - # valid put - content = '{"topic":"Topic name"}' - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, content) - self.assertEquals(200, code, msg=str(response)) - - # valid get - (code, response) = yield self.mock_resource.trigger_get(self.path) - self.assertEquals(200, code, msg=str(response)) - self.assert_dict(json.loads(content), response) - - @defer.inlineCallbacks - def test_rooms_topic_with_extra_keys(self): - # valid put with extra keys - content = '{"topic":"Seasons","subtopic":"Summer"}' - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, content) - self.assertEquals(200, code, msg=str(response)) - - # valid get - (code, response) = yield self.mock_resource.trigger_get(self.path) - self.assertEquals(200, code, msg=str(response)) - self.assert_dict(json.loads(content), response) - - -class RoomMemberStateTestCase(RestTestCase): - """ Tests /rooms/$room_id/members/$user_id/state REST events. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.room.register_servlets(hs, self.mock_resource) - - self.room_id = yield self.create_room_as(self.user_id) - - def tearDown(self): - pass - - @defer.inlineCallbacks - def test_invalid_puts(self): - path = "/rooms/%s/state/m.room.member/%s" % (self.room_id, self.user_id) - # missing keys or invalid json - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{"_name":"bob"}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{"nao') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '[{"_name":"bob"},{"_name":"jill"}]') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, 'text only') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '') - self.assertEquals(400, code, msg=str(response)) - - # valid keys, wrong types - content = ('{"membership":["%s","%s","%s"]}' % - (Membership.INVITE, Membership.JOIN, Membership.LEAVE)) - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(400, code, msg=str(response)) - - @defer.inlineCallbacks - def test_rooms_members_self(self): - path = "/rooms/%s/state/m.room.member/%s" % ( - urllib.quote(self.room_id), self.user_id - ) - - # valid join message (NOOP since we made the room) - content = '{"membership":"%s"}' % Membership.JOIN - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("GET", path, None) - self.assertEquals(200, code, msg=str(response)) - - expected_response = { - "membership": Membership.JOIN, - } - self.assertEquals(expected_response, response) - - @defer.inlineCallbacks - def test_rooms_members_other(self): - self.other_id = "@zzsid1:red" - path = "/rooms/%s/state/m.room.member/%s" % ( - urllib.quote(self.room_id), self.other_id - ) - - # valid invite message - content = '{"membership":"%s"}' % Membership.INVITE - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("GET", path, None) - self.assertEquals(200, code, msg=str(response)) - self.assertEquals(json.loads(content), response) - - @defer.inlineCallbacks - def test_rooms_members_other_custom_keys(self): - self.other_id = "@zzsid1:red" - path = "/rooms/%s/state/m.room.member/%s" % ( - urllib.quote(self.room_id), self.other_id - ) - - # valid invite message with custom key - content = ('{"membership":"%s","invite_text":"%s"}' % - (Membership.INVITE, "Join us!")) - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("GET", path, None) - self.assertEquals(200, code, msg=str(response)) - self.assertEquals(json.loads(content), response) - - -class RoomMessagesTestCase(RestTestCase): - """ Tests /rooms/$room_id/messages/$user_id/$msg_id REST events. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.room.register_servlets(hs, self.mock_resource) - - self.room_id = yield self.create_room_as(self.user_id) - - def tearDown(self): - pass - - @defer.inlineCallbacks - def test_invalid_puts(self): - path = "/rooms/%s/send/m.room.message/mid1" % ( - urllib.quote(self.room_id)) - # missing keys or invalid json - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{"_name":"bob"}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{"nao') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '[{"_name":"bob"},{"_name":"jill"}]') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, 'text only') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '') - self.assertEquals(400, code, msg=str(response)) - - @defer.inlineCallbacks - def test_rooms_messages_sent(self): - path = "/rooms/%s/send/m.room.message/mid1" % ( - urllib.quote(self.room_id)) - - content = '{"body":"test","msgtype":{"type":"a"}}' - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(400, code, msg=str(response)) - - # custom message types - content = '{"body":"test","msgtype":"test.custom.text"}' - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - -# (code, response) = yield self.mock_resource.trigger("GET", path, None) -# self.assertEquals(200, code, msg=str(response)) -# self.assert_dict(json.loads(content), response) - - # m.text message type - path = "/rooms/%s/send/m.room.message/mid2" % ( - urllib.quote(self.room_id)) - content = '{"body":"test2","msgtype":"m.text"}' - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - - -class RoomInitialSyncTestCase(RestTestCase): - """ Tests /rooms/$room_id/initialSync. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.room.register_servlets(hs, self.mock_resource) - - # Since I'm getting my own presence I need to exist as far as presence - # is concerned. - hs.get_handlers().presence_handler.registered_user( - hs.parse_userid(self.user_id) - ) - - # create the room - self.room_id = yield self.create_room_as(self.user_id) - - @defer.inlineCallbacks - def test_initial_sync(self): - (code, response) = yield self.mock_resource.trigger_get( - "/rooms/%s/initialSync" % self.room_id) - self.assertEquals(200, code) - - self.assertEquals(self.room_id, response["room_id"]) - self.assertEquals("join", response["membership"]) - - # Room state is easier to assert on if we unpack it into a dict - state = {} - for event in response["state"]: - if "state_key" not in event: - continue - t = event["type"] - if t not in state: - state[t] = [] - state[t].append(event) - - self.assertTrue("m.room.create" in state) - - self.assertTrue("messages" in response) - self.assertTrue("chunk" in response["messages"]) - self.assertTrue("end" in response["messages"]) - - self.assertTrue("presence" in response) - - presence_by_user = {e["content"]["user_id"]: e - for e in response["presence"] - } - self.assertTrue(self.user_id in presence_by_user) - self.assertEquals("m.presence", presence_by_user[self.user_id]["type"]) diff --git a/tests/rest/test_typing.py b/tests/rest/test_typing.py deleted file mode 100644 index 18138af1b5..0000000000 --- a/tests/rest/test_typing.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /rooms paths.""" - -# twisted imports -from twisted.internet import defer - -import synapse.rest.room -from synapse.server import HomeServer - -from ..utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey -from .utils import RestTestCase - -from mock import Mock, NonCallableMock - - -PATH_PREFIX = "/_matrix/client/api/v1" - - -class RoomTypingTestCase(RestTestCase): - """ Tests /rooms/$room_id/typing/$user_id REST API. """ - user_id = "@sid:red" - - @defer.inlineCallbacks - def setUp(self): - self.clock = MockClock() - - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - clock=self.clock, - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.hs = hs - - self.event_source = hs.get_event_sources().sources["typing"] - - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - def get_room_members(room_id): - if room_id == self.room_id: - return defer.succeed([hs.parse_userid(self.user_id)]) - else: - return defer.succeed([]) - - @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 hs.is_mine(member): - if localusers is not None: - localusers.add(member) - else: - if remotedomains is not None: - remotedomains.add(member.domain) - hs.get_handlers().room_member_handler.fetch_room_distributions_into = ( - fetch_room_distributions_into) - - synapse.rest.room.register_servlets(hs, self.mock_resource) - - self.room_id = yield self.create_room_as(self.user_id) - # Need another user to make notifications actually work - yield self.join(self.room_id, user="@jim:red") - - def tearDown(self): - self.hs.get_handlers().typing_notification_handler.tearDown() - - @defer.inlineCallbacks - def test_set_typing(self): - (code, _) = yield self.mock_resource.trigger("PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - '{"typing": true, "timeout": 30000}' - ) - self.assertEquals(200, code) - - self.assertEquals(self.event_source.get_current_key(), 1) - self.assertEquals( - self.event_source.get_new_events_for_user(self.user_id, 0, None)[0], - [ - {"type": "m.typing", - "room_id": self.room_id, - "content": { - "user_ids": [self.user_id], - }}, - ] - ) - - @defer.inlineCallbacks - def test_set_not_typing(self): - (code, _) = yield self.mock_resource.trigger("PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - '{"typing": false}' - ) - self.assertEquals(200, code) - - @defer.inlineCallbacks - def test_typing_timeout(self): - (code, _) = yield self.mock_resource.trigger("PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - '{"typing": true, "timeout": 30000}' - ) - self.assertEquals(200, code) - - self.assertEquals(self.event_source.get_current_key(), 1) - - self.clock.advance_time(31); - - self.assertEquals(self.event_source.get_current_key(), 2) - - (code, _) = yield self.mock_resource.trigger("PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - '{"typing": true, "timeout": 30000}' - ) - self.assertEquals(200, code) - - self.assertEquals(self.event_source.get_current_key(), 3) diff --git a/tests/rest/utils.py b/tests/rest/utils.py deleted file mode 100644 index 579441fb4a..0000000000 --- a/tests/rest/utils.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# twisted imports -from twisted.internet import defer - -# trial imports -from tests import unittest - -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. - - This subclass assumes there are mock_resource and auth_user_id attributes. - """ - - def __init__(self, *args, **kwargs): - super(RestTestCase, self).__init__(*args, **kwargs) - self.mock_resource = None - self.auth_user_id = None - - def mock_get_user_by_token(self, token=None): - return self.auth_user_id - - @defer.inlineCallbacks - 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 = "/createRoom" - content = "{}" - if not is_public: - content = '{"visibility":"private"}' - if tok: - path = path + "?access_token=%s" % tok - (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): - yield self.change_membership(room=room, src=src, targ=targ, tok=tok, - membership=Membership.INVITE, - expect_code=expect_code) - - @defer.inlineCallbacks - def join(self, room=None, user=None, expect_code=200, tok=None): - yield self.change_membership(room=room, src=user, targ=user, tok=tok, - membership=Membership.JOIN, - expect_code=expect_code) - - @defer.inlineCallbacks - def leave(self, room=None, user=None, expect_code=200, tok=None): - yield self.change_membership(room=room, src=user, targ=user, tok=tok, - membership=Membership.LEAVE, - expect_code=expect_code) - - @defer.inlineCallbacks - def change_membership(self, room, src, targ, membership, tok=None, - expect_code=200): - temp_id = self.auth_user_id - self.auth_user_id = src - - path = "/rooms/%s/state/m.room.member/%s" % (room, targ) - if tok: - path = path + "?access_token=%s" % tok - - data = { - "membership": membership - } - - (code, response) = yield self.mock_resource.trigger("PUT", path, - json.dumps(data)) - self.assertEquals(expect_code, code, msg=str(response)) - - self.auth_user_id = temp_id - - @defer.inlineCallbacks - def register(self, user_id): - (code, response) = yield self.mock_resource.trigger( - "POST", - "/register", - json.dumps({ - "user": user_id, - "password": "test", - "type": "m.login.password" - })) - self.assertEquals(200, code) - defer.returnValue(response) - - @defer.inlineCallbacks - def send(self, room_id, body=None, txn_id=None, tok=None, - expect_code=200): - if txn_id is None: - txn_id = "m%s" % (str(time.time())) - if body is None: - body = "body_text_here" - - path = "/rooms/%s/send/m.room.message/%s" % (room_id, txn_id) - content = '{"msgtype":"m.text","body":"%s"}' % body - if tok: - path = path + "?access_token=%s" % tok - - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(expect_code, code, msg=str(response)) - - def assert_dict(self, required, actual): - """Does a partial assert of a dict. - - Args: - required (dict): The keys and value which MUST be in 'actual'. - actual (dict): The test result. Extra keys will not be checked. - """ - for key in required: - self.assertEquals(required[key], actual[key], - msg="%s mismatch. %s" % (key, actual)) -- cgit 1.5.1 From 97c68c508dac6b4b3203b3bc475ffdfd185b6e03 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 22 Jan 2015 16:10:07 +0000 Subject: Move rest APIs back under the rest directory --- synapse/app/homeserver.py | 4 +- synapse/client/__init__.py | 14 - synapse/client/v1/__init__.py | 47 --- synapse/client/v1/admin.py | 47 --- synapse/client/v1/base.py | 80 ---- synapse/client/v1/directory.py | 112 ------ synapse/client/v1/events.py | 81 ---- synapse/client/v1/initial_sync.py | 44 --- synapse/client/v1/login.py | 109 ------ synapse/client/v1/presence.py | 145 -------- synapse/client/v1/profile.py | 113 ------ synapse/client/v1/register.py | 291 --------------- synapse/client/v1/room.py | 559 ---------------------------- synapse/client/v1/transactions.py | 95 ----- synapse/client/v1/voip.py | 60 --- synapse/media/__init__.py | 0 synapse/media/v0/__init__.py | 0 synapse/media/v0/content_repository.py | 212 ----------- synapse/media/v1/__init__.py | 45 --- synapse/media/v1/base_resource.py | 378 ------------------- synapse/media/v1/download_resource.py | 74 ---- synapse/media/v1/filepath.py | 67 ---- synapse/media/v1/media_repository.py | 77 ---- synapse/media/v1/thumbnail_resource.py | 193 ---------- synapse/media/v1/thumbnailer.py | 89 ----- synapse/media/v1/upload_resource.py | 113 ------ synapse/rest/__init__.py | 14 + synapse/rest/client/__init__.py | 14 + synapse/rest/client/v1/__init__.py | 47 +++ synapse/rest/client/v1/admin.py | 47 +++ synapse/rest/client/v1/base.py | 80 ++++ synapse/rest/client/v1/directory.py | 112 ++++++ synapse/rest/client/v1/events.py | 81 ++++ synapse/rest/client/v1/initial_sync.py | 44 +++ synapse/rest/client/v1/login.py | 109 ++++++ synapse/rest/client/v1/presence.py | 145 ++++++++ synapse/rest/client/v1/profile.py | 113 ++++++ synapse/rest/client/v1/register.py | 291 +++++++++++++++ synapse/rest/client/v1/room.py | 559 ++++++++++++++++++++++++++++ synapse/rest/client/v1/transactions.py | 95 +++++ synapse/rest/client/v1/voip.py | 60 +++ synapse/rest/media/__init__.py | 0 synapse/rest/media/v0/__init__.py | 0 synapse/rest/media/v0/content_repository.py | 212 +++++++++++ synapse/rest/media/v1/__init__.py | 45 +++ synapse/rest/media/v1/base_resource.py | 378 +++++++++++++++++++ synapse/rest/media/v1/download_resource.py | 74 ++++ synapse/rest/media/v1/filepath.py | 67 ++++ synapse/rest/media/v1/media_repository.py | 77 ++++ synapse/rest/media/v1/thumbnail_resource.py | 193 ++++++++++ synapse/rest/media/v1/thumbnailer.py | 89 +++++ synapse/rest/media/v1/upload_resource.py | 113 ++++++ synapse/server.py | 2 +- tests/client/v1/test_events.py | 12 +- tests/client/v1/test_rooms.py | 16 +- tests/client/v1/test_typing.py | 4 +- 56 files changed, 3078 insertions(+), 3064 deletions(-) delete mode 100644 synapse/client/__init__.py delete mode 100644 synapse/client/v1/__init__.py delete mode 100644 synapse/client/v1/admin.py delete mode 100644 synapse/client/v1/base.py delete mode 100644 synapse/client/v1/directory.py delete mode 100644 synapse/client/v1/events.py delete mode 100644 synapse/client/v1/initial_sync.py delete mode 100644 synapse/client/v1/login.py delete mode 100644 synapse/client/v1/presence.py delete mode 100644 synapse/client/v1/profile.py delete mode 100644 synapse/client/v1/register.py delete mode 100644 synapse/client/v1/room.py delete mode 100644 synapse/client/v1/transactions.py delete mode 100644 synapse/client/v1/voip.py delete mode 100644 synapse/media/__init__.py delete mode 100644 synapse/media/v0/__init__.py delete mode 100644 synapse/media/v0/content_repository.py delete mode 100644 synapse/media/v1/__init__.py delete mode 100644 synapse/media/v1/base_resource.py delete mode 100644 synapse/media/v1/download_resource.py delete mode 100644 synapse/media/v1/filepath.py delete mode 100644 synapse/media/v1/media_repository.py delete mode 100644 synapse/media/v1/thumbnail_resource.py delete mode 100644 synapse/media/v1/thumbnailer.py delete mode 100644 synapse/media/v1/upload_resource.py create mode 100644 synapse/rest/__init__.py create mode 100644 synapse/rest/client/__init__.py create mode 100644 synapse/rest/client/v1/__init__.py create mode 100644 synapse/rest/client/v1/admin.py create mode 100644 synapse/rest/client/v1/base.py create mode 100644 synapse/rest/client/v1/directory.py create mode 100644 synapse/rest/client/v1/events.py create mode 100644 synapse/rest/client/v1/initial_sync.py create mode 100644 synapse/rest/client/v1/login.py create mode 100644 synapse/rest/client/v1/presence.py create mode 100644 synapse/rest/client/v1/profile.py create mode 100644 synapse/rest/client/v1/register.py create mode 100644 synapse/rest/client/v1/room.py create mode 100644 synapse/rest/client/v1/transactions.py create mode 100644 synapse/rest/client/v1/voip.py create mode 100644 synapse/rest/media/__init__.py create mode 100644 synapse/rest/media/v0/__init__.py create mode 100644 synapse/rest/media/v0/content_repository.py create mode 100644 synapse/rest/media/v1/__init__.py create mode 100644 synapse/rest/media/v1/base_resource.py create mode 100644 synapse/rest/media/v1/download_resource.py create mode 100644 synapse/rest/media/v1/filepath.py create mode 100644 synapse/rest/media/v1/media_repository.py create mode 100644 synapse/rest/media/v1/thumbnail_resource.py create mode 100644 synapse/rest/media/v1/thumbnailer.py create mode 100644 synapse/rest/media/v1/upload_resource.py diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index afe3d19760..cd24bbdc79 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -26,8 +26,8 @@ from twisted.web.resource import Resource from twisted.web.static import File from twisted.web.server import Site from synapse.http.server import JsonResource, RootRedirect -from synapse.media.v0.content_repository import ContentRepoResource -from synapse.media.v1.media_repository import MediaRepositoryResource +from synapse.rest.media.v0.content_repository import ContentRepoResource +from synapse.rest.media.v1.media_repository import MediaRepositoryResource from synapse.http.server_key_resource import LocalKey from synapse.http.matrixfederationclient import MatrixFederationHttpClient from synapse.api.urls import ( diff --git a/synapse/client/__init__.py b/synapse/client/__init__.py deleted file mode 100644 index 1a84d94cd9..0000000000 --- a/synapse/client/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/synapse/client/v1/__init__.py b/synapse/client/v1/__init__.py deleted file mode 100644 index 88ec9cd27d..0000000000 --- a/synapse/client/v1/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from . import ( - room, events, register, login, profile, presence, initial_sync, directory, - voip, admin, -) - - -class RestServletFactory(object): - - """ A factory for creating REST servlets. - - These REST servlets represent the entire client-server REST API. Generally - speaking, they serve as wrappers around events and the handlers that - process them. - - See synapse.events for information on synapse events. - """ - - def __init__(self, hs): - client_resource = hs.get_resource_for_client() - - # TODO(erikj): There *must* be a better way of doing this. - room.register_servlets(hs, client_resource) - events.register_servlets(hs, client_resource) - register.register_servlets(hs, client_resource) - login.register_servlets(hs, client_resource) - profile.register_servlets(hs, client_resource) - presence.register_servlets(hs, client_resource) - initial_sync.register_servlets(hs, client_resource) - directory.register_servlets(hs, client_resource) - voip.register_servlets(hs, client_resource) - admin.register_servlets(hs, client_resource) diff --git a/synapse/client/v1/admin.py b/synapse/client/v1/admin.py deleted file mode 100644 index 0aa83514c8..0000000000 --- a/synapse/client/v1/admin.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.internet import defer - -from synapse.api.errors import AuthError, SynapseError -from base import RestServlet, client_path_pattern - -import logging - -logger = logging.getLogger(__name__) - - -class WhoisRestServlet(RestServlet): - PATTERN = client_path_pattern("/admin/whois/(?P[^/]*)") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - target_user = self.hs.parse_userid(user_id) - auth_user = yield self.auth.get_user_by_req(request) - is_admin = yield self.auth.is_server_admin(auth_user) - - if not is_admin and target_user != auth_user: - raise AuthError(403, "You are not a server admin") - - if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only whois a local user") - - ret = yield self.handlers.admin_handler.get_whois(target_user) - - defer.returnValue((200, ret)) - - -def register_servlets(hs, http_server): - WhoisRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/base.py b/synapse/client/v1/base.py deleted file mode 100644 index d005206b77..0000000000 --- a/synapse/client/v1/base.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains base REST classes for constructing REST servlets. """ -from synapse.api.urls import CLIENT_PREFIX -from .transactions import HttpTransactionStore -import re - -import logging - - -logger = logging.getLogger(__name__) - - -def client_path_pattern(path_regex): - """Creates a regex compiled client path with the correct client path - prefix. - - Args: - path_regex (str): The regex string to match. This should NOT have a ^ - as this will be prefixed. - Returns: - SRE_Pattern - """ - return re.compile("^" + CLIENT_PREFIX + path_regex) - - -class RestServlet(object): - - """ A Synapse REST Servlet. - - An implementing class can either provide its own custom 'register' method, - or use the automatic pattern handling provided by the base class. - - To use this latter, the implementing class instead provides a `PATTERN` - class attribute containing a pre-compiled regular expression. The automatic - register method will then use this method to register any of the following - instance methods associated with the corresponding HTTP method: - - on_GET - on_PUT - on_POST - on_DELETE - on_OPTIONS - - Automatically handles turning CodeMessageExceptions thrown by these methods - into the appropriate HTTP response. - """ - - def __init__(self, hs): - self.hs = hs - - self.handlers = hs.get_handlers() - self.builder_factory = hs.get_event_builder_factory() - self.auth = hs.get_auth() - self.txns = HttpTransactionStore() - - def register(self, http_server): - """ Register this servlet with the given HTTP server. """ - if hasattr(self, "PATTERN"): - pattern = self.PATTERN - - for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"): - if hasattr(self, "on_%s" % (method)): - method_handler = getattr(self, "on_%s" % (method)) - http_server.register_path(method, pattern, method_handler) - else: - raise NotImplementedError("RestServlet must register something.") diff --git a/synapse/client/v1/directory.py b/synapse/client/v1/directory.py deleted file mode 100644 index 7ff44fdd9e..0000000000 --- a/synapse/client/v1/directory.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from twisted.internet import defer - -from synapse.api.errors import AuthError, SynapseError, Codes -from base import RestServlet, client_path_pattern - -import json -import logging - - -logger = logging.getLogger(__name__) - - -def register_servlets(hs, http_server): - ClientDirectoryServer(hs).register(http_server) - - -class ClientDirectoryServer(RestServlet): - PATTERN = client_path_pattern("/directory/room/(?P[^/]*)$") - - @defer.inlineCallbacks - def on_GET(self, request, room_alias): - room_alias = self.hs.parse_roomalias(room_alias) - - dir_handler = self.handlers.directory_handler - res = yield dir_handler.get_association(room_alias) - - defer.returnValue((200, res)) - - @defer.inlineCallbacks - def on_PUT(self, request, room_alias): - user = yield self.auth.get_user_by_req(request) - - content = _parse_json(request) - if not "room_id" in content: - raise SynapseError(400, "Missing room_id key", - errcode=Codes.BAD_JSON) - - logger.debug("Got content: %s", content) - - room_alias = self.hs.parse_roomalias(room_alias) - - logger.debug("Got room name: %s", room_alias.to_string()) - - room_id = content["room_id"] - servers = content["servers"] if "servers" in content else None - - logger.debug("Got room_id: %s", room_id) - logger.debug("Got servers: %s", servers) - - # TODO(erikj): Check types. - # TODO(erikj): Check that room exists - - dir_handler = self.handlers.directory_handler - - try: - user_id = user.to_string() - yield dir_handler.create_association( - user_id, room_alias, room_id, servers - ) - yield dir_handler.send_room_alias_update_event(user_id, room_id) - except SynapseError as e: - raise e - except: - logger.exception("Failed to create association") - raise - - defer.returnValue((200, {})) - - @defer.inlineCallbacks - def on_DELETE(self, request, room_alias): - user = yield self.auth.get_user_by_req(request) - - is_admin = yield self.auth.is_server_admin(user) - if not is_admin: - raise AuthError(403, "You need to be a server admin") - - dir_handler = self.handlers.directory_handler - - room_alias = self.hs.parse_roomalias(room_alias) - - yield dir_handler.delete_association( - user.to_string(), room_alias - ) - - defer.returnValue((200, {})) - - -def _parse_json(request): - try: - content = json.loads(request.content.read()) - if type(content) != dict: - raise SynapseError(400, "Content must be a JSON object.", - errcode=Codes.NOT_JSON) - return content - except ValueError: - raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) diff --git a/synapse/client/v1/events.py b/synapse/client/v1/events.py deleted file mode 100644 index c2515528ac..0000000000 --- a/synapse/client/v1/events.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains REST servlets to do with event streaming, /events.""" -from twisted.internet import defer - -from synapse.api.errors import SynapseError -from synapse.streams.config import PaginationConfig -from .base import RestServlet, client_path_pattern - -import logging - - -logger = logging.getLogger(__name__) - - -class EventStreamRestServlet(RestServlet): - PATTERN = client_path_pattern("/events$") - - DEFAULT_LONGPOLL_TIME_MS = 30000 - - @defer.inlineCallbacks - def on_GET(self, request): - auth_user = yield self.auth.get_user_by_req(request) - try: - handler = self.handlers.event_stream_handler - pagin_config = PaginationConfig.from_request(request) - timeout = EventStreamRestServlet.DEFAULT_LONGPOLL_TIME_MS - if "timeout" in request.args: - try: - timeout = int(request.args["timeout"][0]) - except ValueError: - raise SynapseError(400, "timeout must be in milliseconds.") - - as_client_event = "raw" not in request.args - - chunk = yield handler.get_stream( - auth_user.to_string(), pagin_config, timeout=timeout, - as_client_event=as_client_event - ) - except: - logger.exception("Event stream failed") - raise - - defer.returnValue((200, chunk)) - - def on_OPTIONS(self, request): - return (200, {}) - - -# TODO: Unit test gets, with and without auth, with different kinds of events. -class EventRestServlet(RestServlet): - PATTERN = client_path_pattern("/events/(?P[^/]*)$") - - @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, self.hs.serialize_event(event))) - 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/client/v1/initial_sync.py b/synapse/client/v1/initial_sync.py deleted file mode 100644 index b13d56b286..0000000000 --- a/synapse/client/v1/initial_sync.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.internet import defer - -from synapse.streams.config import PaginationConfig -from base import RestServlet, client_path_pattern - - -# TODO: Needs unit testing -class InitialSyncRestServlet(RestServlet): - PATTERN = client_path_pattern("/initialSync$") - - @defer.inlineCallbacks - def on_GET(self, request): - user = yield self.auth.get_user_by_req(request) - with_feedback = "feedback" in request.args - as_client_event = "raw" not in request.args - pagination_config = PaginationConfig.from_request(request) - handler = self.handlers.message_handler - content = yield handler.snapshot_all_rooms( - user_id=user.to_string(), - pagin_config=pagination_config, - feedback=with_feedback, - as_client_event=as_client_event - ) - - defer.returnValue((200, content)) - - -def register_servlets(hs, http_server): - InitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/login.py b/synapse/client/v1/login.py deleted file mode 100644 index 6b8deff67b..0000000000 --- a/synapse/client/v1/login.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.internet import defer - -from synapse.api.errors import SynapseError -from synapse.types import UserID -from base import RestServlet, client_path_pattern - -import json - - -class LoginRestServlet(RestServlet): - PATTERN = client_path_pattern("/login$") - PASS_TYPE = "m.login.password" - - def on_GET(self, request): - return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]}) - - def on_OPTIONS(self, request): - return (200, {}) - - @defer.inlineCallbacks - def on_POST(self, request): - login_submission = _parse_json(request) - try: - if login_submission["type"] == LoginRestServlet.PASS_TYPE: - result = yield self.do_password_login(login_submission) - defer.returnValue(result) - else: - raise SynapseError(400, "Bad login type.") - except KeyError: - raise SynapseError(400, "Missing JSON keys.") - - @defer.inlineCallbacks - def do_password_login(self, login_submission): - if not login_submission["user"].startswith('@'): - login_submission["user"] = UserID.create( - login_submission["user"], self.hs.hostname).to_string() - - handler = self.handlers.login_handler - token = yield handler.login( - user=login_submission["user"], - password=login_submission["password"]) - - result = { - "user_id": login_submission["user"], # may have changed - "access_token": token, - "home_server": self.hs.hostname, - } - - defer.returnValue((200, result)) - - -class LoginFallbackRestServlet(RestServlet): - PATTERN = client_path_pattern("/login/fallback$") - - def on_GET(self, request): - # TODO(kegan): This should be returning some HTML which is capable of - # hitting LoginRestServlet - return (200, {}) - - -class PasswordResetRestServlet(RestServlet): - PATTERN = client_path_pattern("/login/reset") - - @defer.inlineCallbacks - def on_POST(self, request): - reset_info = _parse_json(request) - try: - email = reset_info["email"] - user_id = reset_info["user_id"] - handler = self.handlers.login_handler - yield handler.reset_password(user_id, email) - # purposefully give no feedback to avoid people hammering different - # combinations. - defer.returnValue((200, {})) - except KeyError: - raise SynapseError( - 400, - "Missing keys. Requires 'email' and 'user_id'." - ) - - -def _parse_json(request): - try: - content = json.loads(request.content.read()) - if type(content) != dict: - raise SynapseError(400, "Content must be a JSON object.") - return content - except ValueError: - raise SynapseError(400, "Content not JSON.") - - -def register_servlets(hs, http_server): - LoginRestServlet(hs).register(http_server) - # TODO PasswordResetRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/presence.py b/synapse/client/v1/presence.py deleted file mode 100644 index ca4d2d21f0..0000000000 --- a/synapse/client/v1/presence.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with presence: /presence/ -""" -from twisted.internet import defer - -from synapse.api.errors import SynapseError -from base import RestServlet, client_path_pattern - -import json -import logging - -logger = logging.getLogger(__name__) - - -class PresenceStatusRestServlet(RestServlet): - PATTERN = client_path_pattern("/presence/(?P[^/]*)/status") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - state = yield self.handlers.presence_handler.get_state( - target_user=user, auth_user=auth_user) - - defer.returnValue((200, state)) - - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - state = {} - try: - content = json.loads(request.content.read()) - - state["presence"] = content.pop("presence") - - if "status_msg" in content: - state["status_msg"] = content.pop("status_msg") - if not isinstance(state["status_msg"], basestring): - raise SynapseError(400, "status_msg must be a string.") - - if content: - raise KeyError() - except SynapseError as e: - raise e - except: - raise SynapseError(400, "Unable to parse state") - - yield self.handlers.presence_handler.set_state( - target_user=user, auth_user=auth_user, state=state) - - defer.returnValue((200, {})) - - def on_OPTIONS(self, request): - return (200, {}) - - -class PresenceListRestServlet(RestServlet): - PATTERN = client_path_pattern("/presence/list/(?P[^/]*)") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - if not self.hs.is_mine(user): - raise SynapseError(400, "User not hosted on this Home Server") - - if auth_user != user: - raise SynapseError(400, "Cannot get another user's presence list") - - presence = yield self.handlers.presence_handler.get_presence_list( - observer_user=user, accepted=True) - - for p in presence: - observed_user = p.pop("observed_user") - p["user_id"] = observed_user.to_string() - - defer.returnValue((200, presence)) - - @defer.inlineCallbacks - def on_POST(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - if not self.hs.is_mine(user): - raise SynapseError(400, "User not hosted on this Home Server") - - if auth_user != user: - raise SynapseError( - 400, "Cannot modify another user's presence list") - - try: - content = json.loads(request.content.read()) - except: - logger.exception("JSON parse error") - raise SynapseError(400, "Unable to parse content") - - if "invite" in content: - for u in content["invite"]: - if not isinstance(u, basestring): - raise SynapseError(400, "Bad invite value.") - if len(u) == 0: - continue - invited_user = self.hs.parse_userid(u) - yield self.handlers.presence_handler.send_invite( - observer_user=user, observed_user=invited_user - ) - - if "drop" in content: - for u in content["drop"]: - if not isinstance(u, basestring): - raise SynapseError(400, "Bad drop value.") - if len(u) == 0: - continue - dropped_user = self.hs.parse_userid(u) - yield self.handlers.presence_handler.drop( - observer_user=user, observed_user=dropped_user - ) - - defer.returnValue((200, {})) - - def on_OPTIONS(self, request): - return (200, {}) - - -def register_servlets(hs, http_server): - PresenceStatusRestServlet(hs).register(http_server) - PresenceListRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/profile.py b/synapse/client/v1/profile.py deleted file mode 100644 index dc6eb424b0..0000000000 --- a/synapse/client/v1/profile.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with profile: /profile/ """ -from twisted.internet import defer - -from base import RestServlet, client_path_pattern - -import json - - -class ProfileDisplaynameRestServlet(RestServlet): - PATTERN = client_path_pattern("/profile/(?P[^/]*)/displayname") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) - - displayname = yield self.handlers.profile_handler.get_displayname( - user, - ) - - defer.returnValue((200, {"displayname": displayname})) - - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - try: - content = json.loads(request.content.read()) - new_name = content["displayname"] - except: - defer.returnValue((400, "Unable to parse name")) - - yield self.handlers.profile_handler.set_displayname( - user, auth_user, new_name) - - defer.returnValue((200, {})) - - def on_OPTIONS(self, request, user_id): - return (200, {}) - - -class ProfileAvatarURLRestServlet(RestServlet): - PATTERN = client_path_pattern("/profile/(?P[^/]*)/avatar_url") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) - - avatar_url = yield self.handlers.profile_handler.get_avatar_url( - user, - ) - - defer.returnValue((200, {"avatar_url": avatar_url})) - - @defer.inlineCallbacks - def on_PUT(self, request, user_id): - auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) - - try: - content = json.loads(request.content.read()) - new_name = content["avatar_url"] - except: - defer.returnValue((400, "Unable to parse name")) - - yield self.handlers.profile_handler.set_avatar_url( - user, auth_user, new_name) - - defer.returnValue((200, {})) - - def on_OPTIONS(self, request, user_id): - return (200, {}) - - -class ProfileRestServlet(RestServlet): - PATTERN = client_path_pattern("/profile/(?P[^/]*)") - - @defer.inlineCallbacks - def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) - - displayname = yield self.handlers.profile_handler.get_displayname( - user, - ) - avatar_url = yield self.handlers.profile_handler.get_avatar_url( - user, - ) - - defer.returnValue((200, { - "displayname": displayname, - "avatar_url": avatar_url - })) - - -def register_servlets(hs, http_server): - ProfileDisplaynameRestServlet(hs).register(http_server) - ProfileAvatarURLRestServlet(hs).register(http_server) - ProfileRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/register.py b/synapse/client/v1/register.py deleted file mode 100644 index e3b26902d9..0000000000 --- a/synapse/client/v1/register.py +++ /dev/null @@ -1,291 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains REST servlets to do with registration: /register""" -from twisted.internet import defer - -from synapse.api.errors import SynapseError, Codes -from synapse.api.constants import LoginType -from base import RestServlet, client_path_pattern -import synapse.util.stringutils as stringutils - -from synapse.util.async import run_on_reactor - -from hashlib import sha1 -import hmac -import json -import logging -import urllib - -logger = logging.getLogger(__name__) - - -# We ought to be using hmac.compare_digest() but on older pythons it doesn't -# exist. It's a _really minor_ security flaw to use plain string comparison -# because the timing attack is so obscured by all the other code here it's -# unlikely to make much difference -if hasattr(hmac, "compare_digest"): - compare_digest = hmac.compare_digest -else: - compare_digest = lambda a, b: a == b - - -class RegisterRestServlet(RestServlet): - """Handles registration with the home server. - - This servlet is in control of the registration flow; the registration - handler doesn't have a concept of multi-stages or sessions. - """ - - PATTERN = client_path_pattern("/register$") - - def __init__(self, hs): - super(RegisterRestServlet, self).__init__(hs) - # sessions are stored as: - # self.sessions = { - # "session_id" : { __session_dict__ } - # } - # TODO: persistent storage - self.sessions = {} - - def on_GET(self, request): - if self.hs.config.enable_registration_captcha: - return ( - 200, - {"flows": [ - { - "type": LoginType.RECAPTCHA, - "stages": [ - LoginType.RECAPTCHA, - LoginType.EMAIL_IDENTITY, - LoginType.PASSWORD - ] - }, - { - "type": LoginType.RECAPTCHA, - "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] - } - ]} - ) - else: - return ( - 200, - {"flows": [ - { - "type": LoginType.EMAIL_IDENTITY, - "stages": [ - LoginType.EMAIL_IDENTITY, LoginType.PASSWORD - ] - }, - { - "type": LoginType.PASSWORD - } - ]} - ) - - @defer.inlineCallbacks - def on_POST(self, request): - register_json = _parse_json(request) - - session = (register_json["session"] - if "session" in register_json else None) - login_type = None - if "type" not in register_json: - raise SynapseError(400, "Missing 'type' key.") - - try: - login_type = register_json["type"] - stages = { - LoginType.RECAPTCHA: self._do_recaptcha, - LoginType.PASSWORD: self._do_password, - LoginType.EMAIL_IDENTITY: self._do_email_identity - } - - session_info = self._get_session_info(request, session) - logger.debug("%s : session info %s request info %s", - login_type, session_info, register_json) - response = yield stages[login_type]( - request, - register_json, - session_info - ) - - if "access_token" not in response: - # isn't a final response - response["session"] = session_info["id"] - - defer.returnValue((200, response)) - except KeyError as e: - logger.exception(e) - raise SynapseError(400, "Missing JSON keys for login type %s." % ( - login_type, - )) - - def on_OPTIONS(self, request): - return (200, {}) - - def _get_session_info(self, request, session_id): - if not session_id: - # create a new session - while session_id is None or session_id in self.sessions: - session_id = stringutils.random_string(24) - self.sessions[session_id] = { - "id": session_id, - LoginType.EMAIL_IDENTITY: False, - LoginType.RECAPTCHA: False - } - - return self.sessions[session_id] - - def _save_session(self, session): - # TODO: Persistent storage - logger.debug("Saving session %s", session) - self.sessions[session["id"]] = session - - def _remove_session(self, session): - logger.debug("Removing session %s", session) - self.sessions.pop(session["id"]) - - @defer.inlineCallbacks - def _do_recaptcha(self, request, register_json, session): - if not self.hs.config.enable_registration_captcha: - raise SynapseError(400, "Captcha not required.") - - yield self._check_recaptcha(request, register_json, session) - - session[LoginType.RECAPTCHA] = True # mark captcha as done - self._save_session(session) - defer.returnValue({ - "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY] - }) - - @defer.inlineCallbacks - def _check_recaptcha(self, request, register_json, session): - if ("captcha_bypass_hmac" in register_json and - self.hs.config.captcha_bypass_secret): - if "user" not in register_json: - raise SynapseError(400, "Captcha bypass needs 'user'") - - want = hmac.new( - key=self.hs.config.captcha_bypass_secret, - msg=register_json["user"], - digestmod=sha1, - ).hexdigest() - - # str() because otherwise hmac complains that 'unicode' does not - # have the buffer interface - got = str(register_json["captcha_bypass_hmac"]) - - if compare_digest(want, got): - session["user"] = register_json["user"] - defer.returnValue(None) - else: - raise SynapseError( - 400, "Captcha bypass HMAC incorrect", - errcode=Codes.CAPTCHA_NEEDED - ) - - challenge = None - user_response = None - try: - challenge = register_json["challenge"] - user_response = register_json["response"] - except KeyError: - raise SynapseError(400, "Captcha response is required", - errcode=Codes.CAPTCHA_NEEDED) - - ip_addr = self.hs.get_ip_from_request(request) - - handler = self.handlers.registration_handler - yield handler.check_recaptcha( - ip_addr, - self.hs.config.recaptcha_private_key, - challenge, - user_response - ) - - @defer.inlineCallbacks - def _do_email_identity(self, request, register_json, session): - if (self.hs.config.enable_registration_captcha and - not session[LoginType.RECAPTCHA]): - raise SynapseError(400, "Captcha is required.") - - threepidCreds = register_json['threepidCreds'] - handler = self.handlers.registration_handler - logger.debug("Registering email. threepidcreds: %s" % (threepidCreds)) - yield handler.register_email(threepidCreds) - session["threepidCreds"] = threepidCreds # store creds for next stage - session[LoginType.EMAIL_IDENTITY] = True # mark email as done - self._save_session(session) - defer.returnValue({ - "next": LoginType.PASSWORD - }) - - @defer.inlineCallbacks - def _do_password(self, request, register_json, session): - yield run_on_reactor() - if (self.hs.config.enable_registration_captcha and - not session[LoginType.RECAPTCHA]): - # captcha should've been done by this stage! - raise SynapseError(400, "Captcha is required.") - - if ("user" in session and "user" in register_json and - session["user"] != register_json["user"]): - raise SynapseError( - 400, "Cannot change user ID during registration" - ) - - password = register_json["password"].encode("utf-8") - desired_user_id = (register_json["user"].encode("utf-8") - if "user" in register_json else None) - if (desired_user_id - and urllib.quote(desired_user_id) != desired_user_id): - raise SynapseError( - 400, - "User ID must only contain characters which do not " + - "require URL encoding.") - handler = self.handlers.registration_handler - (user_id, token) = yield handler.register( - localpart=desired_user_id, - password=password - ) - - if session[LoginType.EMAIL_IDENTITY]: - logger.debug("Binding emails %s to %s" % ( - session["threepidCreds"], user_id) - ) - yield handler.bind_emails(user_id, session["threepidCreds"]) - - result = { - "user_id": user_id, - "access_token": token, - "home_server": self.hs.hostname, - } - self._remove_session(session) - defer.returnValue(result) - - -def _parse_json(request): - try: - content = json.loads(request.content.read()) - if type(content) != dict: - raise SynapseError(400, "Content must be a JSON object.") - return content - except ValueError: - raise SynapseError(400, "Content not JSON.") - - -def register_servlets(hs, http_server): - RegisterRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/room.py b/synapse/client/v1/room.py deleted file mode 100644 index 48bba2a5f3..0000000000 --- a/synapse/client/v1/room.py +++ /dev/null @@ -1,559 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" This module contains REST servlets to do with rooms: /rooms/ """ -from twisted.internet import defer - -from base import RestServlet, client_path_pattern -from synapse.api.errors import SynapseError, Codes -from synapse.streams.config import PaginationConfig -from synapse.api.constants import EventTypes, Membership - -import json -import logging -import urllib - - -logger = logging.getLogger(__name__) - - -class RoomCreateRestServlet(RestServlet): - # No PATTERN; we have custom dispatch rules here - - def register(self, http_server): - 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, txn_id): - try: - defer.returnValue( - self.txns.get_client_transaction(request, txn_id) - ) - except KeyError: - pass - - response = yield self.on_POST(request) - - self.txns.store_client_transaction(request, txn_id, response) - defer.returnValue(response) - - @defer.inlineCallbacks - def on_POST(self, request): - auth_user = yield self.auth.get_user_by_req(request) - - room_config = self.get_room_config(request) - info = yield self.make_room(room_config, auth_user, None) - room_config.update(info) - defer.returnValue((200, info)) - - @defer.inlineCallbacks - def make_room(self, room_config, auth_user, room_id): - handler = self.handlers.room_creation_handler - info = yield handler.create_room( - user_id=auth_user.to_string(), - room_id=room_id, - config=room_config - ) - defer.returnValue(info) - - def get_room_config(self, request): - try: - user_supplied_config = json.loads(request.content.read()) - if "visibility" not in user_supplied_config: - # default visibility - user_supplied_config["visibility"] = "public" - return user_supplied_config - except (ValueError, TypeError): - raise SynapseError(400, "Body must be JSON.", - errcode=Codes.BAD_JSON) - - def on_OPTIONS(self, request): - return (200, {}) - - -# TODO: Needs unit testing for generic events -class RoomStateEventRestServlet(RestServlet): - def register(self, http_server): - # /room/$roomid/state/$eventtype - no_state_key = "/rooms/(?P[^/]*)/state/(?P[^/]*)$" - - # /room/$roomid/state/$eventtype/$statekey - state_key = ("/rooms/(?P[^/]*)/state/" - "(?P[^/]*)/(?P[^/]*)$") - - http_server.register_path("GET", - client_path_pattern(state_key), - self.on_GET) - http_server.register_path("PUT", - client_path_pattern(state_key), - self.on_PUT) - http_server.register_path("GET", - client_path_pattern(no_state_key), - self.on_GET_no_state_key) - http_server.register_path("PUT", - client_path_pattern(no_state_key), - self.on_PUT_no_state_key) - - def on_GET_no_state_key(self, request, room_id, event_type): - return self.on_GET(request, room_id, event_type, "") - - def on_PUT_no_state_key(self, request, room_id, event_type): - return self.on_PUT(request, room_id, event_type, "") - - @defer.inlineCallbacks - def on_GET(self, request, room_id, event_type, state_key): - user = yield self.auth.get_user_by_req(request) - - msg_handler = self.handlers.message_handler - data = yield msg_handler.get_room_data( - user_id=user.to_string(), - room_id=room_id, - event_type=event_type, - state_key=state_key, - ) - - if not data: - raise SynapseError( - 404, "Event not found.", errcode=Codes.NOT_FOUND - ) - defer.returnValue((200, data.get_dict()["content"])) - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, event_type, state_key): - user = yield self.auth.get_user_by_req(request) - - content = _parse_json(request) - - event_dict = { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": user.to_string(), - } - - if state_key is not None: - event_dict["state_key"] = state_key - - msg_handler = self.handlers.message_handler - yield msg_handler.create_and_send_event(event_dict) - - defer.returnValue((200, {})) - - -# TODO: Needs unit testing for generic events + feedback -class RoomSendEventRestServlet(RestServlet): - - def register(self, http_server): - # /rooms/$roomid/send/$event_type[/$txn_id] - PATTERN = ("/rooms/(?P[^/]*)/send/(?P[^/]*)") - register_txn_path(self, PATTERN, http_server, with_get=True) - - @defer.inlineCallbacks - def on_POST(self, request, room_id, event_type): - user = yield self.auth.get_user_by_req(request) - content = _parse_json(request) - - msg_handler = self.handlers.message_handler - event = yield msg_handler.create_and_send_event( - { - "type": event_type, - "content": content, - "room_id": room_id, - "sender": user.to_string(), - } - ) - - defer.returnValue((200, {"event_id": event.event_id})) - - def on_GET(self, request, room_id, event_type, txn_id): - return (200, "Not implemented") - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, event_type, txn_id): - try: - defer.returnValue( - self.txns.get_client_transaction(request, txn_id) - ) - except KeyError: - pass - - response = yield self.on_POST(request, room_id, event_type) - - self.txns.store_client_transaction(request, txn_id, response) - defer.returnValue(response) - - -# TODO: Needs unit testing for room ID + alias joins -class JoinRoomAliasServlet(RestServlet): - - def register(self, http_server): - # /join/$room_identifier[/$txn_id] - PATTERN = ("/join/(?P[^/]*)") - register_txn_path(self, PATTERN, http_server) - - @defer.inlineCallbacks - def on_POST(self, request, room_identifier): - user = yield self.auth.get_user_by_req(request) - - # 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. - - identifier = None - is_room_alias = False - try: - identifier = self.hs.parse_roomalias(room_identifier) - is_room_alias = True - except SynapseError: - identifier = self.hs.parse_roomid(room_identifier) - - # TODO: Support for specifying the home server to join with? - - 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 - msg_handler = self.handlers.message_handler - yield msg_handler.create_and_send_event( - { - "type": EventTypes.Member, - "content": {"membership": Membership.JOIN}, - "room_id": identifier.to_string(), - "sender": user.to_string(), - "state_key": user.to_string(), - } - ) - - defer.returnValue((200, {"room_id": identifier.to_string()})) - - @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 -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[^/]*)/members$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - # TODO support Pagination stream API (limit/tokens) - user = yield self.auth.get_user_by_req(request) - handler = self.handlers.room_member_handler - members = yield handler.get_room_members_as_pagination_chunk( - room_id=room_id, - user_id=user.to_string()) - - for event in members["chunk"]: - # FIXME: should probably be state_key here, not user_id - target_user = self.hs.parse_userid(event["user_id"]) - # Presence is an optional cache; don't fail if we can't fetch it - try: - presence_handler = self.handlers.presence_handler - presence_state = yield presence_handler.get_state( - target_user=target_user, auth_user=user - ) - event["content"].update(presence_state) - except: - pass - - defer.returnValue((200, members)) - - -# TODO: Needs unit testing -class RoomMessageListRestServlet(RestServlet): - PATTERN = client_path_pattern("/rooms/(?P[^/]*)/messages$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - user = yield self.auth.get_user_by_req(request) - pagination_config = PaginationConfig.from_request( - request, default_limit=10, - ) - with_feedback = "feedback" in request.args - as_client_event = "raw" not in request.args - handler = self.handlers.message_handler - msgs = yield handler.get_messages( - room_id=room_id, - user_id=user.to_string(), - pagin_config=pagination_config, - feedback=with_feedback, - as_client_event=as_client_event - ) - - defer.returnValue((200, msgs)) - - -# TODO: Needs unit testing -class RoomStateRestServlet(RestServlet): - PATTERN = client_path_pattern("/rooms/(?P[^/]*)/state$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - user = yield self.auth.get_user_by_req(request) - handler = self.handlers.message_handler - # Get all the current state for this room - events = yield handler.get_state_events( - room_id=room_id, - user_id=user.to_string(), - ) - defer.returnValue((200, events)) - - -# TODO: Needs unit testing -class RoomInitialSyncRestServlet(RestServlet): - PATTERN = client_path_pattern("/rooms/(?P[^/]*)/initialSync$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - user = yield self.auth.get_user_by_req(request) - pagination_config = PaginationConfig.from_request(request) - content = yield self.handlers.message_handler.room_initial_sync( - room_id=room_id, - user_id=user.to_string(), - pagin_config=pagination_config, - ) - defer.returnValue((200, content)) - - -class RoomTriggerBackfill(RestServlet): - PATTERN = client_path_pattern("/rooms/(?P[^/]*)/backfill$") - - @defer.inlineCallbacks - def on_GET(self, request, room_id): - remote_server = urllib.unquote( - request.args["remote"][0] - ).decode("UTF-8") - - limit = int(request.args["limit"][0]) - - handler = self.handlers.federation_handler - events = yield handler.backfill(remote_server, room_id, limit) - - res = [self.hs.serialize_event(event) for event in events] - defer.returnValue((200, res)) - - -# TODO: Needs unit testing -class RoomMembershipRestServlet(RestServlet): - - def register(self, http_server): - # /rooms/$roomid/[invite|join|leave] - PATTERN = ("/rooms/(?P[^/]*)/" - "(?Pjoin|invite|leave|ban|kick)") - register_txn_path(self, PATTERN, http_server) - - @defer.inlineCallbacks - def on_POST(self, request, room_id, membership_action): - user = yield self.auth.get_user_by_req(request) - - content = _parse_json(request) - - # target user is you unless it is an invite - state_key = user.to_string() - if membership_action in ["invite", "ban", "kick"]: - if "user_id" not in content: - raise SynapseError(400, "Missing user_id key.") - state_key = content["user_id"] - - if membership_action == "kick": - membership_action = "leave" - - msg_handler = self.handlers.message_handler - yield msg_handler.create_and_send_event( - { - "type": EventTypes.Member, - "content": {"membership": unicode(membership_action)}, - "room_id": room_id, - "sender": user.to_string(), - "state_key": state_key, - } - ) - - defer.returnValue((200, {})) - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, membership_action, txn_id): - try: - defer.returnValue( - self.txns.get_client_transaction(request, txn_id) - ) - except KeyError: - pass - - response = yield self.on_POST(request, room_id, membership_action) - - self.txns.store_client_transaction(request, txn_id, response) - defer.returnValue(response) - - -class RoomRedactEventRestServlet(RestServlet): - def register(self, http_server): - PATTERN = ("/rooms/(?P[^/]*)/redact/(?P[^/]*)") - register_txn_path(self, PATTERN, http_server) - - @defer.inlineCallbacks - def on_POST(self, request, room_id, event_id): - user = yield self.auth.get_user_by_req(request) - content = _parse_json(request) - - msg_handler = self.handlers.message_handler - event = yield msg_handler.create_and_send_event( - { - "type": EventTypes.Redaction, - "content": content, - "room_id": room_id, - "sender": user.to_string(), - "redacts": event_id, - } - ) - - defer.returnValue((200, {"event_id": event.event_id})) - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, event_id, txn_id): - try: - defer.returnValue( - self.txns.get_client_transaction(request, txn_id) - ) - except KeyError: - pass - - response = yield self.on_POST(request, room_id, event_id) - - self.txns.store_client_transaction(request, txn_id, response) - defer.returnValue(response) - - -class RoomTypingRestServlet(RestServlet): - PATTERN = client_path_pattern( - "/rooms/(?P[^/]*)/typing/(?P[^/]*)$" - ) - - @defer.inlineCallbacks - def on_PUT(self, request, room_id, user_id): - auth_user = yield self.auth.get_user_by_req(request) - - room_id = urllib.unquote(room_id) - target_user = self.hs.parse_userid(urllib.unquote(user_id)) - - content = _parse_json(request) - - typing_handler = self.handlers.typing_notification_handler - - if content["typing"]: - yield typing_handler.started_typing( - target_user=target_user, - auth_user=auth_user, - room_id=room_id, - timeout=content.get("timeout", 30000), - ) - else: - yield typing_handler.stopped_typing( - target_user=target_user, - auth_user=auth_user, - room_id=room_id, - ) - - defer.returnValue((200, {})) - - -def _parse_json(request): - try: - content = json.loads(request.content.read()) - if type(content) != dict: - raise SynapseError(400, "Content must be a JSON object.", - errcode=Codes.NOT_JSON) - return content - except ValueError: - raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) - - -def register_txn_path(servlet, regex_string, http_server, with_get=False): - """Registers a transaction-based path. - - This registers two paths: - PUT regex_string/$txnid - POST regex_string - - Args: - regex_string (str): The regex string to register. Must NOT have a - trailing $ as this string will be appended to. - http_server : The http_server to register paths with. - with_get: True to also register respective GET paths for the PUTs. - """ - http_server.register_path( - "POST", - client_path_pattern(regex_string + "$"), - servlet.on_POST - ) - http_server.register_path( - "PUT", - client_path_pattern(regex_string + "/(?P[^/]*)$"), - servlet.on_PUT - ) - if with_get: - http_server.register_path( - "GET", - client_path_pattern(regex_string + "/(?P[^/]*)$"), - servlet.on_GET - ) - - -def register_servlets(hs, http_server): - RoomStateEventRestServlet(hs).register(http_server) - RoomCreateRestServlet(hs).register(http_server) - RoomMemberListRestServlet(hs).register(http_server) - RoomMessageListRestServlet(hs).register(http_server) - JoinRoomAliasServlet(hs).register(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) - RoomRedactEventRestServlet(hs).register(http_server) - RoomTypingRestServlet(hs).register(http_server) diff --git a/synapse/client/v1/transactions.py b/synapse/client/v1/transactions.py deleted file mode 100644 index d933fea18a..0000000000 --- a/synapse/client/v1/transactions.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module contains logic for storing HTTP PUT transactions. This is used -to ensure idempotency when performing PUTs using the REST API.""" -import logging - -logger = logging.getLogger(__name__) - - -# FIXME: elsewhere we use FooStore to indicate something in the storage layer... -class HttpTransactionStore(object): - - def __init__(self): - # { key : (txn_id, response) } - self.transactions = {} - - def get_response(self, key, txn_id): - """Retrieve a response for this request. - - Args: - key (str): A transaction-independent key for this request. Usually - this is a combination of the path (without the transaction id) - and the user's access token. - txn_id (str): The transaction ID for this request - Returns: - A tuple of (HTTP response code, response content) or None. - """ - try: - logger.debug("get_response Key: %s TxnId: %s", key, txn_id) - (last_txn_id, response) = self.transactions[key] - if txn_id == last_txn_id: - logger.info("get_response: Returning a response for %s", key) - return response - except KeyError: - pass - return None - - def store_response(self, key, txn_id, response): - """Stores an HTTP response tuple. - - Args: - key (str): A transaction-independent key for this request. Usually - this is a combination of the path (without the transaction id) - and the user's access token. - txn_id (str): The transaction ID for this request. - response (tuple): A tuple of (HTTP response code, response content) - """ - logger.debug("store_response Key: %s TxnId: %s", key, txn_id) - self.transactions[key] = (txn_id, response) - - def store_client_transaction(self, request, txn_id, response): - """Stores the request/response pair of an HTTP transaction. - - Args: - request (twisted.web.http.Request): The twisted HTTP request. This - request must have the transaction ID as the last path segment. - response (tuple): A tuple of (response code, response dict) - txn_id (str): The transaction ID for this request. - """ - self.store_response(self._get_key(request), txn_id, response) - - def get_client_transaction(self, request, txn_id): - """Retrieves a stored response if there was one. - - Args: - request (twisted.web.http.Request): The twisted HTTP request. This - request must have the transaction ID as the last path segment. - txn_id (str): The transaction ID for this request. - Returns: - The response tuple. - Raises: - KeyError if the transaction was not found. - """ - response = self.get_response(self._get_key(request), txn_id) - if response is None: - raise KeyError("Transaction not found.") - return response - - def _get_key(self, request): - token = request.args["access_token"][0] - path_without_txn_id = request.path.rsplit("/", 1)[0] - return path_without_txn_id + "/" + token diff --git a/synapse/client/v1/voip.py b/synapse/client/v1/voip.py deleted file mode 100644 index 011c35e69b..0000000000 --- a/synapse/client/v1/voip.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from twisted.internet import defer - -from base import RestServlet, client_path_pattern - - -import hmac -import hashlib -import base64 - - -class VoipRestServlet(RestServlet): - PATTERN = client_path_pattern("/voip/turnServer$") - - @defer.inlineCallbacks - def on_GET(self, request): - auth_user = yield self.auth.get_user_by_req(request) - - turnUris = self.hs.config.turn_uris - turnSecret = self.hs.config.turn_shared_secret - userLifetime = self.hs.config.turn_user_lifetime - if not turnUris or not turnSecret or not userLifetime: - defer.returnValue((200, {})) - - expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000 - username = "%d:%s" % (expiry, auth_user.to_string()) - - mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1) - # We need to use standard base64 encoding here, *not* syutil's - # encode_base64 because we need to add the standard padding to get the - # same result as the TURN server. - password = base64.b64encode(mac.digest()) - - defer.returnValue((200, { - 'username': username, - 'password': password, - 'ttl': userLifetime / 1000, - 'uris': turnUris, - })) - - def on_OPTIONS(self, request): - return (200, {}) - - -def register_servlets(hs, http_server): - VoipRestServlet(hs).register(http_server) diff --git a/synapse/media/__init__.py b/synapse/media/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/synapse/media/v0/__init__.py b/synapse/media/v0/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/synapse/media/v0/content_repository.py b/synapse/media/v0/content_repository.py deleted file mode 100644 index 79ae0e3d74..0000000000 --- a/synapse/media/v0/content_repository.py +++ /dev/null @@ -1,212 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from synapse.http.server import respond_with_json_bytes - -from synapse.util.stringutils import random_string -from synapse.api.errors import ( - cs_exception, SynapseError, CodeMessageException, Codes, cs_error -) - -from twisted.protocols.basic import FileSender -from twisted.web import server, resource -from twisted.internet import defer - -import base64 -import json -import logging -import os -import re - -logger = logging.getLogger(__name__) - - -class ContentRepoResource(resource.Resource): - """Provides file uploading and downloading. - - Uploads are POSTed to wherever this Resource is linked to. This resource - returns a "content token" which can be used to GET this content again. The - token is typically a path, but it may not be. Tokens can expire, be - one-time uses, etc. - - In this case, the token is a path to the file and contains 3 interesting - sections: - - User ID base64d (for namespacing content to each user) - - random 24 char string - - Content type base64d (so we can return it when clients GET it) - - """ - isLeaf = True - - def __init__(self, hs, directory, auth, external_addr): - resource.Resource.__init__(self) - self.hs = hs - self.directory = directory - self.auth = auth - self.external_addr = external_addr.rstrip('/') - self.max_upload_size = hs.config.max_upload_size - - if not os.path.isdir(self.directory): - os.mkdir(self.directory) - logger.info("ContentRepoResource : Created %s directory.", - self.directory) - - @defer.inlineCallbacks - def map_request_to_name(self, request): - # auth the user - auth_user = yield self.auth.get_user_by_req(request) - - # namespace all file uploads on the user - prefix = base64.urlsafe_b64encode( - auth_user.to_string() - ).replace('=', '') - - # use a random string for the main portion - main_part = random_string(24) - - # suffix with a file extension if we can make one. This is nice to - # provide a hint to clients on the file information. We will also reuse - # this info to spit back the content type to the client. - suffix = "" - if request.requestHeaders.hasHeader("Content-Type"): - content_type = request.requestHeaders.getRawHeaders( - "Content-Type")[0] - suffix = "." + base64.urlsafe_b64encode(content_type) - if (content_type.split("/")[0].lower() in - ["image", "video", "audio"]): - file_ext = content_type.split("/")[-1] - # be a little paranoid and only allow a-z - file_ext = re.sub("[^a-z]", "", file_ext) - suffix += "." + file_ext - - file_name = prefix + main_part + suffix - file_path = os.path.join(self.directory, file_name) - logger.info("User %s is uploading a file to path %s", - auth_user.to_string(), - file_path) - - # keep trying to make a non-clashing file, with a sensible max attempts - attempts = 0 - while os.path.exists(file_path): - main_part = random_string(24) - file_name = prefix + main_part + suffix - file_path = os.path.join(self.directory, file_name) - attempts += 1 - if attempts > 25: # really? Really? - raise SynapseError(500, "Unable to create file.") - - defer.returnValue(file_path) - - def render_GET(self, request): - # no auth here on purpose, to allow anyone to view, even across home - # servers. - - # TODO: A little crude here, we could do this better. - filename = request.path.split('/')[-1] - # be paranoid - filename = re.sub("[^0-9A-z.-_]", "", filename) - - file_path = self.directory + "/" + filename - - logger.debug("Searching for %s", file_path) - - if os.path.isfile(file_path): - # filename has the content type - base64_contentype = filename.split(".")[1] - content_type = base64.urlsafe_b64decode(base64_contentype) - logger.info("Sending file %s", file_path) - f = open(file_path, 'rb') - request.setHeader('Content-Type', content_type) - - # cache for at least a day. - # XXX: we might want to turn this off for data we don't want to - # recommend caching as it's sensitive or private - or at least - # select private. don't bother setting Expires as all our matrix - # clients are smart enough to be happy with Cache-Control (right?) - request.setHeader( - "Cache-Control", "public,max-age=86400,s-maxage=86400" - ) - - d = FileSender().beginFileTransfer(f, request) - - # after the file has been sent, clean up and finish the request - def cbFinished(ignored): - f.close() - request.finish() - d.addCallback(cbFinished) - else: - respond_with_json_bytes( - request, - 404, - json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)), - send_cors=True) - - return server.NOT_DONE_YET - - def render_POST(self, request): - self._async_render(request) - return server.NOT_DONE_YET - - def render_OPTIONS(self, request): - respond_with_json_bytes(request, 200, {}, send_cors=True) - return server.NOT_DONE_YET - - @defer.inlineCallbacks - def _async_render(self, request): - try: - # TODO: The checks here are a bit late. The content will have - # already been uploaded to a tmp file at this point - content_length = request.getHeader("Content-Length") - if content_length is None: - raise SynapseError( - msg="Request must specify a Content-Length", code=400 - ) - if int(content_length) > self.max_upload_size: - raise SynapseError( - msg="Upload request body is too large", - code=413, - ) - - fname = yield self.map_request_to_name(request) - - # TODO I have a suspicious feeling this is just going to block - with open(fname, "wb") as f: - f.write(request.content.read()) - - # FIXME (erikj): These should use constants. - file_name = os.path.basename(fname) - # FIXME: we can't assume what the repo's public mounted path is - # ...plus self-signed SSL won't work to remote clients anyway - # ...and we can't assume that it's SSL anyway, as we might want to - # serve it via the non-SSL listener... - url = "%s/_matrix/content/%s" % ( - self.external_addr, file_name - ) - - respond_with_json_bytes(request, 200, - json.dumps({"content_token": url}), - send_cors=True) - - except CodeMessageException as e: - logger.exception(e) - respond_with_json_bytes(request, e.code, - json.dumps(cs_exception(e))) - except Exception as e: - logger.error("Failed to store file: %s" % e) - respond_with_json_bytes( - request, - 500, - json.dumps({"error": "Internal server error"}), - send_cors=True) diff --git a/synapse/media/v1/__init__.py b/synapse/media/v1/__init__.py deleted file mode 100644 index d6c6690577..0000000000 --- a/synapse/media/v1/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import PIL.Image - -# check for JPEG support. -try: - PIL.Image._getdecoder("rgb", "jpeg", None) -except IOError as e: - if str(e).startswith("decoder jpeg not available"): - raise Exception( - "FATAL: jpeg codec not supported. Install pillow correctly! " - " 'sudo apt-get install libjpeg-dev' then 'pip uninstall pillow &&" - " pip install pillow --user'" - ) -except Exception: - # any other exception is fine - pass - - -# check for PNG support. -try: - PIL.Image._getdecoder("rgb", "zip", None) -except IOError as e: - if str(e).startswith("decoder zip not available"): - raise Exception( - "FATAL: zip codec not supported. Install pillow correctly! " - " 'sudo apt-get install libjpeg-dev' then 'pip uninstall pillow &&" - " pip install pillow --user'" - ) -except Exception: - # any other exception is fine - pass diff --git a/synapse/media/v1/base_resource.py b/synapse/media/v1/base_resource.py deleted file mode 100644 index 688e7376ad..0000000000 --- a/synapse/media/v1/base_resource.py +++ /dev/null @@ -1,378 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .thumbnailer import Thumbnailer - -from synapse.http.server import respond_with_json -from synapse.util.stringutils import random_string -from synapse.api.errors import ( - cs_exception, CodeMessageException, cs_error, Codes, SynapseError -) - -from twisted.internet import defer -from twisted.web.resource import Resource -from twisted.protocols.basic import FileSender - -import os - -import logging - -logger = logging.getLogger(__name__) - - -class BaseMediaResource(Resource): - isLeaf = True - - def __init__(self, hs, filepaths): - Resource.__init__(self) - self.auth = hs.get_auth() - self.client = hs.get_http_client() - self.clock = hs.get_clock() - self.server_name = hs.hostname - self.store = hs.get_datastore() - self.max_upload_size = hs.config.max_upload_size - self.max_image_pixels = hs.config.max_image_pixels - self.filepaths = filepaths - self.downloads = {} - - @staticmethod - def catch_errors(request_handler): - @defer.inlineCallbacks - def wrapped_request_handler(self, request): - try: - yield request_handler(self, request) - except CodeMessageException as e: - logger.exception(e) - respond_with_json( - request, e.code, cs_exception(e), send_cors=True - ) - except: - logger.exception( - "Failed handle request %s.%s on %r", - request_handler.__module__, - request_handler.__name__, - self, - ) - respond_with_json( - request, - 500, - {"error": "Internal server error"}, - send_cors=True - ) - return wrapped_request_handler - - @staticmethod - def _parse_media_id(request): - try: - server_name, media_id = request.postpath - return (server_name, media_id) - except: - raise SynapseError( - 404, - "Invalid media id token %r" % (request.postpath,), - Codes.UNKKOWN, - ) - - @staticmethod - def _parse_integer(request, arg_name, default=None): - try: - if default is None: - return int(request.args[arg_name][0]) - else: - return int(request.args.get(arg_name, [default])[0]) - except: - raise SynapseError( - 400, - "Missing integer argument %r" % (arg_name,), - Codes.UNKNOWN, - ) - - @staticmethod - def _parse_string(request, arg_name, default=None): - try: - if default is None: - return request.args[arg_name][0] - else: - return request.args.get(arg_name, [default])[0] - except: - raise SynapseError( - 400, - "Missing string argument %r" % (arg_name,), - Codes.UNKNOWN, - ) - - def _respond_404(self, request): - respond_with_json( - request, 404, - cs_error( - "Not found %r" % (request.postpath,), - code=Codes.NOT_FOUND, - ), - send_cors=True - ) - - @staticmethod - def _makedirs(filepath): - dirname = os.path.dirname(filepath) - if not os.path.exists(dirname): - os.makedirs(dirname) - - def _get_remote_media(self, server_name, media_id): - key = (server_name, media_id) - download = self.downloads.get(key) - if download is None: - download = self._get_remote_media_impl(server_name, media_id) - self.downloads[key] = download - - @download.addBoth - def callback(media_info): - del self.downloads[key] - return media_info - return download - - @defer.inlineCallbacks - def _get_remote_media_impl(self, server_name, media_id): - media_info = yield self.store.get_cached_remote_media( - server_name, media_id - ) - if not media_info: - media_info = yield self._download_remote_file( - server_name, media_id - ) - defer.returnValue(media_info) - - @defer.inlineCallbacks - def _download_remote_file(self, server_name, media_id): - file_id = random_string(24) - - fname = self.filepaths.remote_media_filepath( - server_name, file_id - ) - self._makedirs(fname) - - try: - with open(fname, "wb") as f: - request_path = "/".join(( - "/_matrix/media/v1/download", server_name, media_id, - )) - length, headers = yield self.client.get_file( - server_name, request_path, output_stream=f, - max_size=self.max_upload_size, - ) - media_type = headers["Content-Type"][0] - time_now_ms = self.clock.time_msec() - - yield self.store.store_cached_remote_media( - origin=server_name, - media_id=media_id, - media_type=media_type, - time_now_ms=self.clock.time_msec(), - upload_name=None, - media_length=length, - filesystem_id=file_id, - ) - except: - os.remove(fname) - raise - - media_info = { - "media_type": media_type, - "media_length": length, - "upload_name": None, - "created_ts": time_now_ms, - "filesystem_id": file_id, - } - - yield self._generate_remote_thumbnails( - server_name, media_id, media_info - ) - - defer.returnValue(media_info) - - @defer.inlineCallbacks - def _respond_with_file(self, request, media_type, file_path, - file_size=None): - logger.debug("Responding with %r", file_path) - - if os.path.isfile(file_path): - request.setHeader(b"Content-Type", media_type.encode("UTF-8")) - - # cache for at least a day. - # XXX: we might want to turn this off for data we don't want to - # recommend caching as it's sensitive or private - or at least - # select private. don't bother setting Expires as all our - # clients are smart enough to be happy with Cache-Control - request.setHeader( - b"Cache-Control", b"public,max-age=86400,s-maxage=86400" - ) - if file_size is None: - stat = os.stat(file_path) - file_size = stat.st_size - - request.setHeader( - b"Content-Length", b"%d" % (file_size,) - ) - - with open(file_path, "rb") as f: - yield FileSender().beginFileTransfer(f, request) - - request.finish() - else: - self._respond_404(request) - - def _get_thumbnail_requirements(self, media_type): - if media_type == "image/jpeg": - return ( - (32, 32, "crop", "image/jpeg"), - (96, 96, "crop", "image/jpeg"), - (320, 240, "scale", "image/jpeg"), - (640, 480, "scale", "image/jpeg"), - ) - elif (media_type == "image/png") or (media_type == "image/gif"): - return ( - (32, 32, "crop", "image/png"), - (96, 96, "crop", "image/png"), - (320, 240, "scale", "image/png"), - (640, 480, "scale", "image/png"), - ) - else: - return () - - @defer.inlineCallbacks - def _generate_local_thumbnails(self, media_id, media_info): - media_type = media_info["media_type"] - requirements = self._get_thumbnail_requirements(media_type) - if not requirements: - return - - input_path = self.filepaths.local_media_filepath(media_id) - thumbnailer = Thumbnailer(input_path) - m_width = thumbnailer.width - m_height = thumbnailer.height - - if m_width * m_height >= self.max_image_pixels: - logger.info( - "Image too large to thumbnail %r x %r > %r", - m_width, m_height, self.max_image_pixels - ) - return - - scales = set() - crops = set() - for r_width, r_height, r_method, r_type in requirements: - if r_method == "scale": - t_width, t_height = thumbnailer.aspect(r_width, r_height) - scales.add(( - min(m_width, t_width), min(m_height, t_height), r_type, - )) - elif r_method == "crop": - crops.add((r_width, r_height, r_type)) - - for t_width, t_height, t_type in scales: - t_method = "scale" - t_path = self.filepaths.local_media_thumbnail( - media_id, t_width, t_height, t_type, t_method - ) - self._makedirs(t_path) - t_len = thumbnailer.scale(t_path, t_width, t_height, t_type) - yield self.store.store_local_thumbnail( - media_id, t_width, t_height, t_type, t_method, t_len - ) - - for t_width, t_height, t_type in crops: - if (t_width, t_height, t_type) in scales: - # If the aspect ratio of the cropped thumbnail matches a purely - # scaled one then there is no point in calculating a separate - # thumbnail. - continue - t_method = "crop" - t_path = self.filepaths.local_media_thumbnail( - media_id, t_width, t_height, t_type, t_method - ) - self._makedirs(t_path) - t_len = thumbnailer.crop(t_path, t_width, t_height, t_type) - yield self.store.store_local_thumbnail( - media_id, t_width, t_height, t_type, t_method, t_len - ) - - defer.returnValue({ - "width": m_width, - "height": m_height, - }) - - @defer.inlineCallbacks - def _generate_remote_thumbnails(self, server_name, media_id, media_info): - media_type = media_info["media_type"] - file_id = media_info["filesystem_id"] - requirements = self._get_thumbnail_requirements(media_type) - if not requirements: - return - - input_path = self.filepaths.remote_media_filepath(server_name, file_id) - thumbnailer = Thumbnailer(input_path) - m_width = thumbnailer.width - m_height = thumbnailer.height - - if m_width * m_height >= self.max_image_pixels: - logger.info( - "Image too large to thumbnail %r x %r > %r", - m_width, m_height, self.max_image_pixels - ) - return - - scales = set() - crops = set() - for r_width, r_height, r_method, r_type in requirements: - if r_method == "scale": - t_width, t_height = thumbnailer.aspect(r_width, r_height) - scales.add(( - min(m_width, t_width), min(m_height, t_height), r_type, - )) - elif r_method == "crop": - crops.add((r_width, r_height, r_type)) - - for t_width, t_height, t_type in scales: - t_method = "scale" - t_path = self.filepaths.remote_media_thumbnail( - server_name, file_id, t_width, t_height, t_type, t_method - ) - self._makedirs(t_path) - t_len = thumbnailer.scale(t_path, t_width, t_height, t_type) - yield self.store.store_remote_media_thumbnail( - server_name, media_id, file_id, - t_width, t_height, t_type, t_method, t_len - ) - - for t_width, t_height, t_type in crops: - if (t_width, t_height, t_type) in scales: - # If the aspect ratio of the cropped thumbnail matches a purely - # scaled one then there is no point in calculating a separate - # thumbnail. - continue - t_method = "crop" - t_path = self.filepaths.remote_media_thumbnail( - server_name, file_id, t_width, t_height, t_type, t_method - ) - self._makedirs(t_path) - t_len = thumbnailer.crop(t_path, t_width, t_height, t_type) - yield self.store.store_remote_media_thumbnail( - server_name, media_id, file_id, - t_width, t_height, t_type, t_method, t_len - ) - - defer.returnValue({ - "width": m_width, - "height": m_height, - }) diff --git a/synapse/media/v1/download_resource.py b/synapse/media/v1/download_resource.py deleted file mode 100644 index c585bb11f7..0000000000 --- a/synapse/media/v1/download_resource.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .base_resource import BaseMediaResource - -from twisted.web.server import NOT_DONE_YET -from twisted.internet import defer - -import logging - -logger = logging.getLogger(__name__) - - -class DownloadResource(BaseMediaResource): - def render_GET(self, request): - self._async_render_GET(request) - return NOT_DONE_YET - - @BaseMediaResource.catch_errors - @defer.inlineCallbacks - def _async_render_GET(self, request): - try: - server_name, media_id = request.postpath - except: - self._respond_404(request) - return - - if server_name == self.server_name: - yield self._respond_local_file(request, media_id) - else: - yield self._respond_remote_file(request, server_name, media_id) - - @defer.inlineCallbacks - def _respond_local_file(self, request, media_id): - media_info = yield self.store.get_local_media(media_id) - if not media_info: - self._respond_404(request) - return - - media_type = media_info["media_type"] - media_length = media_info["media_length"] - file_path = self.filepaths.local_media_filepath(media_id) - - yield self._respond_with_file( - request, media_type, file_path, media_length - ) - - @defer.inlineCallbacks - def _respond_remote_file(self, request, server_name, media_id): - media_info = yield self._get_remote_media(server_name, media_id) - - media_type = media_info["media_type"] - media_length = media_info["media_length"] - filesystem_id = media_info["filesystem_id"] - - file_path = self.filepaths.remote_media_filepath( - server_name, filesystem_id - ) - - yield self._respond_with_file( - request, media_type, file_path, media_length - ) diff --git a/synapse/media/v1/filepath.py b/synapse/media/v1/filepath.py deleted file mode 100644 index ed9a58e9d9..0000000000 --- a/synapse/media/v1/filepath.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - - -class MediaFilePaths(object): - - def __init__(self, base_path): - self.base_path = base_path - - def default_thumbnail(self, default_top_level, default_sub_type, width, - height, content_type, method): - top_level_type, sub_type = content_type.split("/") - file_name = "%i-%i-%s-%s-%s" % ( - width, height, top_level_type, sub_type, method - ) - return os.path.join( - self.base_path, "default_thumbnails", default_top_level, - default_sub_type, file_name - ) - - def local_media_filepath(self, media_id): - return os.path.join( - self.base_path, "local_content", - media_id[0:2], media_id[2:4], media_id[4:] - ) - - def local_media_thumbnail(self, media_id, width, height, content_type, - method): - top_level_type, sub_type = content_type.split("/") - file_name = "%i-%i-%s-%s-%s" % ( - width, height, top_level_type, sub_type, method - ) - return os.path.join( - self.base_path, "local_thumbnails", - media_id[0:2], media_id[2:4], media_id[4:], - file_name - ) - - def remote_media_filepath(self, server_name, file_id): - return os.path.join( - self.base_path, "remote_content", server_name, - file_id[0:2], file_id[2:4], file_id[4:] - ) - - def remote_media_thumbnail(self, server_name, file_id, width, height, - content_type, method): - top_level_type, sub_type = content_type.split("/") - file_name = "%i-%i-%s-%s" % (width, height, top_level_type, sub_type) - return os.path.join( - self.base_path, "remote_thumbnail", server_name, - file_id[0:2], file_id[2:4], file_id[4:], - file_name - ) diff --git a/synapse/media/v1/media_repository.py b/synapse/media/v1/media_repository.py deleted file mode 100644 index 461cc001f1..0000000000 --- a/synapse/media/v1/media_repository.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .upload_resource import UploadResource -from .download_resource import DownloadResource -from .thumbnail_resource import ThumbnailResource -from .filepath import MediaFilePaths - -from twisted.web.resource import Resource - -import logging - -logger = logging.getLogger(__name__) - - -class MediaRepositoryResource(Resource): - """File uploading and downloading. - - Uploads are POSTed to a resource which returns a token which is used to GET - the download:: - - => POST /_matrix/media/v1/upload HTTP/1.1 - Content-Type: - - - - <= HTTP/1.1 200 OK - Content-Type: application/json - - { "content_uri": "mxc:///" } - - => GET /_matrix/media/v1/download// HTTP/1.1 - - <= HTTP/1.1 200 OK - Content-Type: - Content-Disposition: attachment;filename= - - - - Clients can get thumbnails by supplying a desired width and height and - thumbnailing method:: - - => GET /_matrix/media/v1/thumbnail/ - /?width=&height=&method= HTTP/1.1 - - <= HTTP/1.1 200 OK - Content-Type: image/jpeg or image/png - - - - The thumbnail methods are "crop" and "scale". "scale" trys to return an - image where either the width or the height is smaller than the requested - size. The client should then scale and letterbox the image if it needs to - fit within a given rectangle. "crop" trys to return an image where the - width and height are close to the requested size and the aspect matches - the requested size. The client should scale the image if it needs to fit - within a given rectangle. - """ - - def __init__(self, hs): - Resource.__init__(self) - filepaths = MediaFilePaths(hs.config.media_store_path) - self.putChild("upload", UploadResource(hs, filepaths)) - self.putChild("download", DownloadResource(hs, filepaths)) - self.putChild("thumbnail", ThumbnailResource(hs, filepaths)) diff --git a/synapse/media/v1/thumbnail_resource.py b/synapse/media/v1/thumbnail_resource.py deleted file mode 100644 index 84f5e3463c..0000000000 --- a/synapse/media/v1/thumbnail_resource.py +++ /dev/null @@ -1,193 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from .base_resource import BaseMediaResource - -from twisted.web.server import NOT_DONE_YET -from twisted.internet import defer - -import logging - -logger = logging.getLogger(__name__) - - -class ThumbnailResource(BaseMediaResource): - isLeaf = True - - def render_GET(self, request): - self._async_render_GET(request) - return NOT_DONE_YET - - @BaseMediaResource.catch_errors - @defer.inlineCallbacks - def _async_render_GET(self, request): - server_name, media_id = self._parse_media_id(request) - width = self._parse_integer(request, "width") - height = self._parse_integer(request, "height") - method = self._parse_string(request, "method", "scale") - m_type = self._parse_string(request, "type", "image/png") - - if server_name == self.server_name: - yield self._respond_local_thumbnail( - request, media_id, width, height, method, m_type - ) - else: - yield self._respond_remote_thumbnail( - request, server_name, media_id, - width, height, method, m_type - ) - - @defer.inlineCallbacks - def _respond_local_thumbnail(self, request, media_id, width, height, - method, m_type): - media_info = yield self.store.get_local_media(media_id) - - if not media_info: - self._respond_404(request) - return - - thumbnail_infos = yield self.store.get_local_media_thumbnails(media_id) - - if thumbnail_infos: - thumbnail_info = self._select_thumbnail( - width, height, method, m_type, thumbnail_infos - ) - t_width = thumbnail_info["thumbnail_width"] - t_height = thumbnail_info["thumbnail_height"] - t_type = thumbnail_info["thumbnail_type"] - t_method = thumbnail_info["thumbnail_method"] - - file_path = self.filepaths.local_media_thumbnail( - media_id, t_width, t_height, t_type, t_method, - ) - yield self._respond_with_file(request, t_type, file_path) - - else: - yield self._respond_default_thumbnail( - request, media_info, width, height, method, m_type, - ) - - @defer.inlineCallbacks - def _respond_remote_thumbnail(self, request, server_name, media_id, width, - height, method, m_type): - # TODO: Don't download the whole remote file - # We should proxy the thumbnail from the remote server instead. - media_info = yield self._get_remote_media(server_name, media_id) - - thumbnail_infos = yield self.store.get_remote_media_thumbnails( - server_name, media_id, - ) - - if thumbnail_infos: - thumbnail_info = self._select_thumbnail( - width, height, method, m_type, thumbnail_infos - ) - t_width = thumbnail_info["thumbnail_width"] - t_height = thumbnail_info["thumbnail_height"] - t_type = thumbnail_info["thumbnail_type"] - t_method = thumbnail_info["thumbnail_method"] - file_id = thumbnail_info["filesystem_id"] - t_length = thumbnail_info["thumbnail_length"] - - file_path = self.filepaths.remote_media_thumbnail( - server_name, file_id, t_width, t_height, t_type, t_method, - ) - yield self._respond_with_file(request, t_type, file_path, t_length) - else: - yield self._respond_default_thumbnail( - request, media_info, width, height, method, m_type, - ) - - @defer.inlineCallbacks - def _respond_default_thumbnail(self, request, media_info, width, height, - method, m_type): - media_type = media_info["media_type"] - top_level_type = media_type.split("/")[0] - sub_type = media_type.split("/")[-1].split(";")[0] - thumbnail_infos = yield self.store.get_default_thumbnails( - top_level_type, sub_type, - ) - if not thumbnail_infos: - thumbnail_infos = yield self.store.get_default_thumbnails( - top_level_type, "_default", - ) - if not thumbnail_infos: - thumbnail_infos = yield self.store.get_default_thumbnails( - "_default", "_default", - ) - if not thumbnail_infos: - self._respond_404(request) - return - - thumbnail_info = self._select_thumbnail( - width, height, "crop", m_type, thumbnail_infos - ) - - t_width = thumbnail_info["thumbnail_width"] - t_height = thumbnail_info["thumbnail_height"] - t_type = thumbnail_info["thumbnail_type"] - t_method = thumbnail_info["thumbnail_method"] - t_length = thumbnail_info["thumbnail_length"] - - file_path = self.filepaths.default_thumbnail( - top_level_type, sub_type, t_width, t_height, t_type, t_method, - ) - yield self.respond_with_file(request, t_type, file_path, t_length) - - def _select_thumbnail(self, desired_width, desired_height, desired_method, - desired_type, thumbnail_infos): - d_w = desired_width - d_h = desired_height - - if desired_method.lower() == "crop": - info_list = [] - for info in thumbnail_infos: - t_w = info["thumbnail_width"] - t_h = info["thumbnail_height"] - t_method = info["thumbnail_method"] - if t_method == "scale" or t_method == "crop": - aspect_quality = abs(d_w * t_h - d_h * t_w) - size_quality = abs((d_w - t_w) * (d_h - t_h)) - type_quality = desired_type != info["thumbnail_type"] - length_quality = info["thumbnail_length"] - info_list.append(( - aspect_quality, size_quality, type_quality, - length_quality, info - )) - if info_list: - return min(info_list)[-1] - else: - info_list = [] - info_list2 = [] - for info in thumbnail_infos: - t_w = info["thumbnail_width"] - t_h = info["thumbnail_height"] - t_method = info["thumbnail_method"] - size_quality = abs((d_w - t_w) * (d_h - t_h)) - type_quality = desired_type != info["thumbnail_type"] - length_quality = info["thumbnail_length"] - if t_method == "scale" and (t_w >= d_w or t_h >= d_h): - info_list.append(( - size_quality, type_quality, length_quality, info - )) - elif t_method == "scale": - info_list2.append(( - size_quality, type_quality, length_quality, info - )) - if info_list: - return min(info_list)[-1] - else: - return min(info_list2)[-1] diff --git a/synapse/media/v1/thumbnailer.py b/synapse/media/v1/thumbnailer.py deleted file mode 100644 index 28404f2b7b..0000000000 --- a/synapse/media/v1/thumbnailer.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import PIL.Image as Image -from io import BytesIO - - -class Thumbnailer(object): - - FORMATS = { - "image/jpeg": "JPEG", - "image/png": "PNG", - } - - def __init__(self, input_path): - self.image = Image.open(input_path) - self.width, self.height = self.image.size - - def aspect(self, max_width, max_height): - """Calculate the largest size that preserves aspect ratio which - fits within the given rectangle:: - - (w_in / h_in) = (w_out / h_out) - w_out = min(w_max, h_max * (w_in / h_in)) - h_out = min(h_max, w_max * (h_in / w_in)) - - Args: - max_width: The largest possible width. - max_height: The larget possible height. - """ - - if max_width * self.height < max_height * self.width: - return (max_width, (max_width * self.height) // self.width) - else: - return ((max_height * self.width) // self.height, max_height) - - def scale(self, output_path, width, height, output_type): - """Rescales the image to the given dimensions""" - scaled = self.image.resize((width, height), Image.ANTIALIAS) - return self.save_image(scaled, output_type, output_path) - - def crop(self, output_path, width, height, output_type): - """Rescales and crops the image to the given dimensions preserving - aspect:: - (w_in / h_in) = (w_scaled / h_scaled) - w_scaled = max(w_out, h_out * (w_in / h_in)) - h_scaled = max(h_out, w_out * (h_in / w_in)) - - Args: - max_width: The largest possible width. - max_height: The larget possible height. - """ - if width * self.height > height * self.width: - scaled_height = (width * self.height) // self.width - scaled_image = self.image.resize( - (width, scaled_height), Image.ANTIALIAS - ) - crop_top = (scaled_height - height) // 2 - crop_bottom = height + crop_top - cropped = scaled_image.crop((0, crop_top, width, crop_bottom)) - else: - scaled_width = (height * self.width) // self.height - scaled_image = self.image.resize( - (scaled_width, height), Image.ANTIALIAS - ) - crop_left = (scaled_width - width) // 2 - crop_right = width + crop_left - cropped = scaled_image.crop((crop_left, 0, crop_right, height)) - return self.save_image(cropped, output_type, output_path) - - def save_image(self, output_image, output_type, output_path): - output_bytes_io = BytesIO() - output_image.save(output_bytes_io, self.FORMATS[output_type], quality=70) - output_bytes = output_bytes_io.getvalue() - with open(output_path, "wb") as output_file: - output_file.write(output_bytes) - return len(output_bytes) diff --git a/synapse/media/v1/upload_resource.py b/synapse/media/v1/upload_resource.py deleted file mode 100644 index b1718a630b..0000000000 --- a/synapse/media/v1/upload_resource.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from synapse.http.server import respond_with_json - -from synapse.util.stringutils import random_string -from synapse.api.errors import ( - cs_exception, SynapseError, CodeMessageException -) - -from twisted.web.server import NOT_DONE_YET -from twisted.internet import defer - -from .base_resource import BaseMediaResource - -import logging - -logger = logging.getLogger(__name__) - - -class UploadResource(BaseMediaResource): - def render_POST(self, request): - self._async_render_POST(request) - return NOT_DONE_YET - - def render_OPTIONS(self, request): - respond_with_json(request, 200, {}, send_cors=True) - return NOT_DONE_YET - - @defer.inlineCallbacks - def _async_render_POST(self, request): - try: - auth_user = yield self.auth.get_user_by_req(request) - # TODO: The checks here are a bit late. The content will have - # already been uploaded to a tmp file at this point - content_length = request.getHeader("Content-Length") - if content_length is None: - raise SynapseError( - msg="Request must specify a Content-Length", code=400 - ) - if int(content_length) > self.max_upload_size: - raise SynapseError( - msg="Upload request body is too large", - code=413, - ) - - headers = request.requestHeaders - - if headers.hasHeader("Content-Type"): - media_type = headers.getRawHeaders("Content-Type")[0] - else: - raise SynapseError( - msg="Upload request missing 'Content-Type'", - code=400, - ) - - #if headers.hasHeader("Content-Disposition"): - # disposition = headers.getRawHeaders("Content-Disposition")[0] - # TODO(markjh): parse content-dispostion - - media_id = random_string(24) - - fname = self.filepaths.local_media_filepath(media_id) - self._makedirs(fname) - - # This shouldn't block for very long because the content will have - # already been uploaded at this point. - with open(fname, "wb") as f: - f.write(request.content.read()) - - yield self.store.store_local_media( - media_id=media_id, - media_type=media_type, - time_now_ms=self.clock.time_msec(), - upload_name=None, - media_length=content_length, - user_id=auth_user, - ) - media_info = { - "media_type": media_type, - "media_length": content_length, - } - - yield self._generate_local_thumbnails(media_id, media_info) - - content_uri = "mxc://%s/%s" % (self.server_name, media_id) - - respond_with_json( - request, 200, {"content_uri": content_uri}, send_cors=True - ) - except CodeMessageException as e: - logger.exception(e) - respond_with_json(request, e.code, cs_exception(e), send_cors=True) - except: - logger.exception("Failed to store file") - respond_with_json( - request, - 500, - {"error": "Internal server error"}, - send_cors=True - ) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py new file mode 100644 index 0000000000..1a84d94cd9 --- /dev/null +++ b/synapse/rest/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/synapse/rest/client/__init__.py b/synapse/rest/client/__init__.py new file mode 100644 index 0000000000..1a84d94cd9 --- /dev/null +++ b/synapse/rest/client/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/synapse/rest/client/v1/__init__.py b/synapse/rest/client/v1/__init__.py new file mode 100644 index 0000000000..88ec9cd27d --- /dev/null +++ b/synapse/rest/client/v1/__init__.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from . import ( + room, events, register, login, profile, presence, initial_sync, directory, + voip, admin, +) + + +class RestServletFactory(object): + + """ A factory for creating REST servlets. + + These REST servlets represent the entire client-server REST API. Generally + speaking, they serve as wrappers around events and the handlers that + process them. + + See synapse.events for information on synapse events. + """ + + def __init__(self, hs): + client_resource = hs.get_resource_for_client() + + # TODO(erikj): There *must* be a better way of doing this. + room.register_servlets(hs, client_resource) + events.register_servlets(hs, client_resource) + register.register_servlets(hs, client_resource) + login.register_servlets(hs, client_resource) + profile.register_servlets(hs, client_resource) + presence.register_servlets(hs, client_resource) + initial_sync.register_servlets(hs, client_resource) + directory.register_servlets(hs, client_resource) + voip.register_servlets(hs, client_resource) + admin.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/v1/admin.py b/synapse/rest/client/v1/admin.py new file mode 100644 index 0000000000..0aa83514c8 --- /dev/null +++ b/synapse/rest/client/v1/admin.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from synapse.api.errors import AuthError, SynapseError +from base import RestServlet, client_path_pattern + +import logging + +logger = logging.getLogger(__name__) + + +class WhoisRestServlet(RestServlet): + PATTERN = client_path_pattern("/admin/whois/(?P[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + target_user = self.hs.parse_userid(user_id) + auth_user = yield self.auth.get_user_by_req(request) + is_admin = yield self.auth.is_server_admin(auth_user) + + if not is_admin and target_user != auth_user: + raise AuthError(403, "You are not a server admin") + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only whois a local user") + + ret = yield self.handlers.admin_handler.get_whois(target_user) + + defer.returnValue((200, ret)) + + +def register_servlets(hs, http_server): + WhoisRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/base.py b/synapse/rest/client/v1/base.py new file mode 100644 index 0000000000..d005206b77 --- /dev/null +++ b/synapse/rest/client/v1/base.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains base REST classes for constructing REST servlets. """ +from synapse.api.urls import CLIENT_PREFIX +from .transactions import HttpTransactionStore +import re + +import logging + + +logger = logging.getLogger(__name__) + + +def client_path_pattern(path_regex): + """Creates a regex compiled client path with the correct client path + prefix. + + Args: + path_regex (str): The regex string to match. This should NOT have a ^ + as this will be prefixed. + Returns: + SRE_Pattern + """ + return re.compile("^" + CLIENT_PREFIX + path_regex) + + +class RestServlet(object): + + """ A Synapse REST Servlet. + + An implementing class can either provide its own custom 'register' method, + or use the automatic pattern handling provided by the base class. + + To use this latter, the implementing class instead provides a `PATTERN` + class attribute containing a pre-compiled regular expression. The automatic + register method will then use this method to register any of the following + instance methods associated with the corresponding HTTP method: + + on_GET + on_PUT + on_POST + on_DELETE + on_OPTIONS + + Automatically handles turning CodeMessageExceptions thrown by these methods + into the appropriate HTTP response. + """ + + def __init__(self, hs): + self.hs = hs + + self.handlers = hs.get_handlers() + self.builder_factory = hs.get_event_builder_factory() + self.auth = hs.get_auth() + self.txns = HttpTransactionStore() + + def register(self, http_server): + """ Register this servlet with the given HTTP server. """ + if hasattr(self, "PATTERN"): + pattern = self.PATTERN + + for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"): + if hasattr(self, "on_%s" % (method)): + method_handler = getattr(self, "on_%s" % (method)) + http_server.register_path(method, pattern, method_handler) + else: + raise NotImplementedError("RestServlet must register something.") diff --git a/synapse/rest/client/v1/directory.py b/synapse/rest/client/v1/directory.py new file mode 100644 index 0000000000..7ff44fdd9e --- /dev/null +++ b/synapse/rest/client/v1/directory.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from twisted.internet import defer + +from synapse.api.errors import AuthError, SynapseError, Codes +from base import RestServlet, client_path_pattern + +import json +import logging + + +logger = logging.getLogger(__name__) + + +def register_servlets(hs, http_server): + ClientDirectoryServer(hs).register(http_server) + + +class ClientDirectoryServer(RestServlet): + PATTERN = client_path_pattern("/directory/room/(?P[^/]*)$") + + @defer.inlineCallbacks + def on_GET(self, request, room_alias): + room_alias = self.hs.parse_roomalias(room_alias) + + dir_handler = self.handlers.directory_handler + res = yield dir_handler.get_association(room_alias) + + defer.returnValue((200, res)) + + @defer.inlineCallbacks + def on_PUT(self, request, room_alias): + user = yield self.auth.get_user_by_req(request) + + content = _parse_json(request) + if not "room_id" in content: + raise SynapseError(400, "Missing room_id key", + errcode=Codes.BAD_JSON) + + logger.debug("Got content: %s", content) + + room_alias = self.hs.parse_roomalias(room_alias) + + logger.debug("Got room name: %s", room_alias.to_string()) + + room_id = content["room_id"] + servers = content["servers"] if "servers" in content else None + + logger.debug("Got room_id: %s", room_id) + logger.debug("Got servers: %s", servers) + + # TODO(erikj): Check types. + # TODO(erikj): Check that room exists + + dir_handler = self.handlers.directory_handler + + try: + user_id = user.to_string() + yield dir_handler.create_association( + user_id, room_alias, room_id, servers + ) + yield dir_handler.send_room_alias_update_event(user_id, room_id) + except SynapseError as e: + raise e + except: + logger.exception("Failed to create association") + raise + + defer.returnValue((200, {})) + + @defer.inlineCallbacks + def on_DELETE(self, request, room_alias): + user = yield self.auth.get_user_by_req(request) + + is_admin = yield self.auth.is_server_admin(user) + if not is_admin: + raise AuthError(403, "You need to be a server admin") + + dir_handler = self.handlers.directory_handler + + room_alias = self.hs.parse_roomalias(room_alias) + + yield dir_handler.delete_association( + user.to_string(), room_alias + ) + + defer.returnValue((200, {})) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.", + errcode=Codes.NOT_JSON) + return content + except ValueError: + raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/v1/events.py new file mode 100644 index 0000000000..c2515528ac --- /dev/null +++ b/synapse/rest/client/v1/events.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains REST servlets to do with event streaming, /events.""" +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from synapse.streams.config import PaginationConfig +from .base import RestServlet, client_path_pattern + +import logging + + +logger = logging.getLogger(__name__) + + +class EventStreamRestServlet(RestServlet): + PATTERN = client_path_pattern("/events$") + + DEFAULT_LONGPOLL_TIME_MS = 30000 + + @defer.inlineCallbacks + def on_GET(self, request): + auth_user = yield self.auth.get_user_by_req(request) + try: + handler = self.handlers.event_stream_handler + pagin_config = PaginationConfig.from_request(request) + timeout = EventStreamRestServlet.DEFAULT_LONGPOLL_TIME_MS + if "timeout" in request.args: + try: + timeout = int(request.args["timeout"][0]) + except ValueError: + raise SynapseError(400, "timeout must be in milliseconds.") + + as_client_event = "raw" not in request.args + + chunk = yield handler.get_stream( + auth_user.to_string(), pagin_config, timeout=timeout, + as_client_event=as_client_event + ) + except: + logger.exception("Event stream failed") + raise + + defer.returnValue((200, chunk)) + + def on_OPTIONS(self, request): + return (200, {}) + + +# TODO: Unit test gets, with and without auth, with different kinds of events. +class EventRestServlet(RestServlet): + PATTERN = client_path_pattern("/events/(?P[^/]*)$") + + @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, self.hs.serialize_event(event))) + 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/client/v1/initial_sync.py b/synapse/rest/client/v1/initial_sync.py new file mode 100644 index 0000000000..b13d56b286 --- /dev/null +++ b/synapse/rest/client/v1/initial_sync.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from synapse.streams.config import PaginationConfig +from base import RestServlet, client_path_pattern + + +# TODO: Needs unit testing +class InitialSyncRestServlet(RestServlet): + PATTERN = client_path_pattern("/initialSync$") + + @defer.inlineCallbacks + def on_GET(self, request): + user = yield self.auth.get_user_by_req(request) + with_feedback = "feedback" in request.args + as_client_event = "raw" not in request.args + pagination_config = PaginationConfig.from_request(request) + handler = self.handlers.message_handler + content = yield handler.snapshot_all_rooms( + user_id=user.to_string(), + pagin_config=pagination_config, + feedback=with_feedback, + as_client_event=as_client_event + ) + + defer.returnValue((200, content)) + + +def register_servlets(hs, http_server): + InitialSyncRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py new file mode 100644 index 0000000000..6b8deff67b --- /dev/null +++ b/synapse/rest/client/v1/login.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from synapse.types import UserID +from base import RestServlet, client_path_pattern + +import json + + +class LoginRestServlet(RestServlet): + PATTERN = client_path_pattern("/login$") + PASS_TYPE = "m.login.password" + + def on_GET(self, request): + return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]}) + + def on_OPTIONS(self, request): + return (200, {}) + + @defer.inlineCallbacks + def on_POST(self, request): + login_submission = _parse_json(request) + try: + if login_submission["type"] == LoginRestServlet.PASS_TYPE: + result = yield self.do_password_login(login_submission) + defer.returnValue(result) + else: + raise SynapseError(400, "Bad login type.") + except KeyError: + raise SynapseError(400, "Missing JSON keys.") + + @defer.inlineCallbacks + def do_password_login(self, login_submission): + if not login_submission["user"].startswith('@'): + login_submission["user"] = UserID.create( + login_submission["user"], self.hs.hostname).to_string() + + handler = self.handlers.login_handler + token = yield handler.login( + user=login_submission["user"], + password=login_submission["password"]) + + result = { + "user_id": login_submission["user"], # may have changed + "access_token": token, + "home_server": self.hs.hostname, + } + + defer.returnValue((200, result)) + + +class LoginFallbackRestServlet(RestServlet): + PATTERN = client_path_pattern("/login/fallback$") + + def on_GET(self, request): + # TODO(kegan): This should be returning some HTML which is capable of + # hitting LoginRestServlet + return (200, {}) + + +class PasswordResetRestServlet(RestServlet): + PATTERN = client_path_pattern("/login/reset") + + @defer.inlineCallbacks + def on_POST(self, request): + reset_info = _parse_json(request) + try: + email = reset_info["email"] + user_id = reset_info["user_id"] + handler = self.handlers.login_handler + yield handler.reset_password(user_id, email) + # purposefully give no feedback to avoid people hammering different + # combinations. + defer.returnValue((200, {})) + except KeyError: + raise SynapseError( + 400, + "Missing keys. Requires 'email' and 'user_id'." + ) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.") + return content + except ValueError: + raise SynapseError(400, "Content not JSON.") + + +def register_servlets(hs, http_server): + LoginRestServlet(hs).register(http_server) + # TODO PasswordResetRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/v1/presence.py new file mode 100644 index 0000000000..ca4d2d21f0 --- /dev/null +++ b/synapse/rest/client/v1/presence.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with presence: /presence/ +""" +from twisted.internet import defer + +from synapse.api.errors import SynapseError +from base import RestServlet, client_path_pattern + +import json +import logging + +logger = logging.getLogger(__name__) + + +class PresenceStatusRestServlet(RestServlet): + PATTERN = client_path_pattern("/presence/(?P[^/]*)/status") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + state = yield self.handlers.presence_handler.get_state( + target_user=user, auth_user=auth_user) + + defer.returnValue((200, state)) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + state = {} + try: + content = json.loads(request.content.read()) + + state["presence"] = content.pop("presence") + + if "status_msg" in content: + state["status_msg"] = content.pop("status_msg") + if not isinstance(state["status_msg"], basestring): + raise SynapseError(400, "status_msg must be a string.") + + if content: + raise KeyError() + except SynapseError as e: + raise e + except: + raise SynapseError(400, "Unable to parse state") + + yield self.handlers.presence_handler.set_state( + target_user=user, auth_user=auth_user, state=state) + + defer.returnValue((200, {})) + + def on_OPTIONS(self, request): + return (200, {}) + + +class PresenceListRestServlet(RestServlet): + PATTERN = client_path_pattern("/presence/list/(?P[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + if not self.hs.is_mine(user): + raise SynapseError(400, "User not hosted on this Home Server") + + if auth_user != user: + raise SynapseError(400, "Cannot get another user's presence list") + + presence = yield self.handlers.presence_handler.get_presence_list( + observer_user=user, accepted=True) + + for p in presence: + observed_user = p.pop("observed_user") + p["user_id"] = observed_user.to_string() + + defer.returnValue((200, presence)) + + @defer.inlineCallbacks + def on_POST(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + if not self.hs.is_mine(user): + raise SynapseError(400, "User not hosted on this Home Server") + + if auth_user != user: + raise SynapseError( + 400, "Cannot modify another user's presence list") + + try: + content = json.loads(request.content.read()) + except: + logger.exception("JSON parse error") + raise SynapseError(400, "Unable to parse content") + + if "invite" in content: + for u in content["invite"]: + if not isinstance(u, basestring): + raise SynapseError(400, "Bad invite value.") + if len(u) == 0: + continue + invited_user = self.hs.parse_userid(u) + yield self.handlers.presence_handler.send_invite( + observer_user=user, observed_user=invited_user + ) + + if "drop" in content: + for u in content["drop"]: + if not isinstance(u, basestring): + raise SynapseError(400, "Bad drop value.") + if len(u) == 0: + continue + dropped_user = self.hs.parse_userid(u) + yield self.handlers.presence_handler.drop( + observer_user=user, observed_user=dropped_user + ) + + defer.returnValue((200, {})) + + def on_OPTIONS(self, request): + return (200, {}) + + +def register_servlets(hs, http_server): + PresenceStatusRestServlet(hs).register(http_server) + PresenceListRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py new file mode 100644 index 0000000000..dc6eb424b0 --- /dev/null +++ b/synapse/rest/client/v1/profile.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with profile: /profile/ """ +from twisted.internet import defer + +from base import RestServlet, client_path_pattern + +import json + + +class ProfileDisplaynameRestServlet(RestServlet): + PATTERN = client_path_pattern("/profile/(?P[^/]*)/displayname") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + user = self.hs.parse_userid(user_id) + + displayname = yield self.handlers.profile_handler.get_displayname( + user, + ) + + defer.returnValue((200, {"displayname": displayname})) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + try: + content = json.loads(request.content.read()) + new_name = content["displayname"] + except: + defer.returnValue((400, "Unable to parse name")) + + yield self.handlers.profile_handler.set_displayname( + user, auth_user, new_name) + + defer.returnValue((200, {})) + + def on_OPTIONS(self, request, user_id): + return (200, {}) + + +class ProfileAvatarURLRestServlet(RestServlet): + PATTERN = client_path_pattern("/profile/(?P[^/]*)/avatar_url") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + user = self.hs.parse_userid(user_id) + + avatar_url = yield self.handlers.profile_handler.get_avatar_url( + user, + ) + + defer.returnValue((200, {"avatar_url": avatar_url})) + + @defer.inlineCallbacks + def on_PUT(self, request, user_id): + auth_user = yield self.auth.get_user_by_req(request) + user = self.hs.parse_userid(user_id) + + try: + content = json.loads(request.content.read()) + new_name = content["avatar_url"] + except: + defer.returnValue((400, "Unable to parse name")) + + yield self.handlers.profile_handler.set_avatar_url( + user, auth_user, new_name) + + defer.returnValue((200, {})) + + def on_OPTIONS(self, request, user_id): + return (200, {}) + + +class ProfileRestServlet(RestServlet): + PATTERN = client_path_pattern("/profile/(?P[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + user = self.hs.parse_userid(user_id) + + displayname = yield self.handlers.profile_handler.get_displayname( + user, + ) + avatar_url = yield self.handlers.profile_handler.get_avatar_url( + user, + ) + + defer.returnValue((200, { + "displayname": displayname, + "avatar_url": avatar_url + })) + + +def register_servlets(hs, http_server): + ProfileDisplaynameRestServlet(hs).register(http_server) + ProfileAvatarURLRestServlet(hs).register(http_server) + ProfileRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/register.py b/synapse/rest/client/v1/register.py new file mode 100644 index 0000000000..e3b26902d9 --- /dev/null +++ b/synapse/rest/client/v1/register.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains REST servlets to do with registration: /register""" +from twisted.internet import defer + +from synapse.api.errors import SynapseError, Codes +from synapse.api.constants import LoginType +from base import RestServlet, client_path_pattern +import synapse.util.stringutils as stringutils + +from synapse.util.async import run_on_reactor + +from hashlib import sha1 +import hmac +import json +import logging +import urllib + +logger = logging.getLogger(__name__) + + +# We ought to be using hmac.compare_digest() but on older pythons it doesn't +# exist. It's a _really minor_ security flaw to use plain string comparison +# because the timing attack is so obscured by all the other code here it's +# unlikely to make much difference +if hasattr(hmac, "compare_digest"): + compare_digest = hmac.compare_digest +else: + compare_digest = lambda a, b: a == b + + +class RegisterRestServlet(RestServlet): + """Handles registration with the home server. + + This servlet is in control of the registration flow; the registration + handler doesn't have a concept of multi-stages or sessions. + """ + + PATTERN = client_path_pattern("/register$") + + def __init__(self, hs): + super(RegisterRestServlet, self).__init__(hs) + # sessions are stored as: + # self.sessions = { + # "session_id" : { __session_dict__ } + # } + # TODO: persistent storage + self.sessions = {} + + def on_GET(self, request): + if self.hs.config.enable_registration_captcha: + return ( + 200, + {"flows": [ + { + "type": LoginType.RECAPTCHA, + "stages": [ + LoginType.RECAPTCHA, + LoginType.EMAIL_IDENTITY, + LoginType.PASSWORD + ] + }, + { + "type": LoginType.RECAPTCHA, + "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD] + } + ]} + ) + else: + return ( + 200, + {"flows": [ + { + "type": LoginType.EMAIL_IDENTITY, + "stages": [ + LoginType.EMAIL_IDENTITY, LoginType.PASSWORD + ] + }, + { + "type": LoginType.PASSWORD + } + ]} + ) + + @defer.inlineCallbacks + def on_POST(self, request): + register_json = _parse_json(request) + + session = (register_json["session"] + if "session" in register_json else None) + login_type = None + if "type" not in register_json: + raise SynapseError(400, "Missing 'type' key.") + + try: + login_type = register_json["type"] + stages = { + LoginType.RECAPTCHA: self._do_recaptcha, + LoginType.PASSWORD: self._do_password, + LoginType.EMAIL_IDENTITY: self._do_email_identity + } + + session_info = self._get_session_info(request, session) + logger.debug("%s : session info %s request info %s", + login_type, session_info, register_json) + response = yield stages[login_type]( + request, + register_json, + session_info + ) + + if "access_token" not in response: + # isn't a final response + response["session"] = session_info["id"] + + defer.returnValue((200, response)) + except KeyError as e: + logger.exception(e) + raise SynapseError(400, "Missing JSON keys for login type %s." % ( + login_type, + )) + + def on_OPTIONS(self, request): + return (200, {}) + + def _get_session_info(self, request, session_id): + if not session_id: + # create a new session + while session_id is None or session_id in self.sessions: + session_id = stringutils.random_string(24) + self.sessions[session_id] = { + "id": session_id, + LoginType.EMAIL_IDENTITY: False, + LoginType.RECAPTCHA: False + } + + return self.sessions[session_id] + + def _save_session(self, session): + # TODO: Persistent storage + logger.debug("Saving session %s", session) + self.sessions[session["id"]] = session + + def _remove_session(self, session): + logger.debug("Removing session %s", session) + self.sessions.pop(session["id"]) + + @defer.inlineCallbacks + def _do_recaptcha(self, request, register_json, session): + if not self.hs.config.enable_registration_captcha: + raise SynapseError(400, "Captcha not required.") + + yield self._check_recaptcha(request, register_json, session) + + session[LoginType.RECAPTCHA] = True # mark captcha as done + self._save_session(session) + defer.returnValue({ + "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY] + }) + + @defer.inlineCallbacks + def _check_recaptcha(self, request, register_json, session): + if ("captcha_bypass_hmac" in register_json and + self.hs.config.captcha_bypass_secret): + if "user" not in register_json: + raise SynapseError(400, "Captcha bypass needs 'user'") + + want = hmac.new( + key=self.hs.config.captcha_bypass_secret, + msg=register_json["user"], + digestmod=sha1, + ).hexdigest() + + # str() because otherwise hmac complains that 'unicode' does not + # have the buffer interface + got = str(register_json["captcha_bypass_hmac"]) + + if compare_digest(want, got): + session["user"] = register_json["user"] + defer.returnValue(None) + else: + raise SynapseError( + 400, "Captcha bypass HMAC incorrect", + errcode=Codes.CAPTCHA_NEEDED + ) + + challenge = None + user_response = None + try: + challenge = register_json["challenge"] + user_response = register_json["response"] + except KeyError: + raise SynapseError(400, "Captcha response is required", + errcode=Codes.CAPTCHA_NEEDED) + + ip_addr = self.hs.get_ip_from_request(request) + + handler = self.handlers.registration_handler + yield handler.check_recaptcha( + ip_addr, + self.hs.config.recaptcha_private_key, + challenge, + user_response + ) + + @defer.inlineCallbacks + def _do_email_identity(self, request, register_json, session): + if (self.hs.config.enable_registration_captcha and + not session[LoginType.RECAPTCHA]): + raise SynapseError(400, "Captcha is required.") + + threepidCreds = register_json['threepidCreds'] + handler = self.handlers.registration_handler + logger.debug("Registering email. threepidcreds: %s" % (threepidCreds)) + yield handler.register_email(threepidCreds) + session["threepidCreds"] = threepidCreds # store creds for next stage + session[LoginType.EMAIL_IDENTITY] = True # mark email as done + self._save_session(session) + defer.returnValue({ + "next": LoginType.PASSWORD + }) + + @defer.inlineCallbacks + def _do_password(self, request, register_json, session): + yield run_on_reactor() + if (self.hs.config.enable_registration_captcha and + not session[LoginType.RECAPTCHA]): + # captcha should've been done by this stage! + raise SynapseError(400, "Captcha is required.") + + if ("user" in session and "user" in register_json and + session["user"] != register_json["user"]): + raise SynapseError( + 400, "Cannot change user ID during registration" + ) + + password = register_json["password"].encode("utf-8") + desired_user_id = (register_json["user"].encode("utf-8") + if "user" in register_json else None) + if (desired_user_id + and urllib.quote(desired_user_id) != desired_user_id): + raise SynapseError( + 400, + "User ID must only contain characters which do not " + + "require URL encoding.") + handler = self.handlers.registration_handler + (user_id, token) = yield handler.register( + localpart=desired_user_id, + password=password + ) + + if session[LoginType.EMAIL_IDENTITY]: + logger.debug("Binding emails %s to %s" % ( + session["threepidCreds"], user_id) + ) + yield handler.bind_emails(user_id, session["threepidCreds"]) + + result = { + "user_id": user_id, + "access_token": token, + "home_server": self.hs.hostname, + } + self._remove_session(session) + defer.returnValue(result) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.") + return content + except ValueError: + raise SynapseError(400, "Content not JSON.") + + +def register_servlets(hs, http_server): + RegisterRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py new file mode 100644 index 0000000000..48bba2a5f3 --- /dev/null +++ b/synapse/rest/client/v1/room.py @@ -0,0 +1,559 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains REST servlets to do with rooms: /rooms/ """ +from twisted.internet import defer + +from base import RestServlet, client_path_pattern +from synapse.api.errors import SynapseError, Codes +from synapse.streams.config import PaginationConfig +from synapse.api.constants import EventTypes, Membership + +import json +import logging +import urllib + + +logger = logging.getLogger(__name__) + + +class RoomCreateRestServlet(RestServlet): + # No PATTERN; we have custom dispatch rules here + + def register(self, http_server): + 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, txn_id): + try: + defer.returnValue( + self.txns.get_client_transaction(request, txn_id) + ) + except KeyError: + pass + + response = yield self.on_POST(request) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + + @defer.inlineCallbacks + def on_POST(self, request): + auth_user = yield self.auth.get_user_by_req(request) + + room_config = self.get_room_config(request) + info = yield self.make_room(room_config, auth_user, None) + room_config.update(info) + defer.returnValue((200, info)) + + @defer.inlineCallbacks + def make_room(self, room_config, auth_user, room_id): + handler = self.handlers.room_creation_handler + info = yield handler.create_room( + user_id=auth_user.to_string(), + room_id=room_id, + config=room_config + ) + defer.returnValue(info) + + def get_room_config(self, request): + try: + user_supplied_config = json.loads(request.content.read()) + if "visibility" not in user_supplied_config: + # default visibility + user_supplied_config["visibility"] = "public" + return user_supplied_config + except (ValueError, TypeError): + raise SynapseError(400, "Body must be JSON.", + errcode=Codes.BAD_JSON) + + def on_OPTIONS(self, request): + return (200, {}) + + +# TODO: Needs unit testing for generic events +class RoomStateEventRestServlet(RestServlet): + def register(self, http_server): + # /room/$roomid/state/$eventtype + no_state_key = "/rooms/(?P[^/]*)/state/(?P[^/]*)$" + + # /room/$roomid/state/$eventtype/$statekey + state_key = ("/rooms/(?P[^/]*)/state/" + "(?P[^/]*)/(?P[^/]*)$") + + http_server.register_path("GET", + client_path_pattern(state_key), + self.on_GET) + http_server.register_path("PUT", + client_path_pattern(state_key), + self.on_PUT) + http_server.register_path("GET", + client_path_pattern(no_state_key), + self.on_GET_no_state_key) + http_server.register_path("PUT", + client_path_pattern(no_state_key), + self.on_PUT_no_state_key) + + def on_GET_no_state_key(self, request, room_id, event_type): + return self.on_GET(request, room_id, event_type, "") + + def on_PUT_no_state_key(self, request, room_id, event_type): + return self.on_PUT(request, room_id, event_type, "") + + @defer.inlineCallbacks + def on_GET(self, request, room_id, event_type, state_key): + user = yield self.auth.get_user_by_req(request) + + msg_handler = self.handlers.message_handler + data = yield msg_handler.get_room_data( + user_id=user.to_string(), + room_id=room_id, + event_type=event_type, + state_key=state_key, + ) + + if not data: + raise SynapseError( + 404, "Event not found.", errcode=Codes.NOT_FOUND + ) + defer.returnValue((200, data.get_dict()["content"])) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, event_type, state_key): + user = yield self.auth.get_user_by_req(request) + + content = _parse_json(request) + + event_dict = { + "type": event_type, + "content": content, + "room_id": room_id, + "sender": user.to_string(), + } + + if state_key is not None: + event_dict["state_key"] = state_key + + msg_handler = self.handlers.message_handler + yield msg_handler.create_and_send_event(event_dict) + + defer.returnValue((200, {})) + + +# TODO: Needs unit testing for generic events + feedback +class RoomSendEventRestServlet(RestServlet): + + def register(self, http_server): + # /rooms/$roomid/send/$event_type[/$txn_id] + PATTERN = ("/rooms/(?P[^/]*)/send/(?P[^/]*)") + register_txn_path(self, PATTERN, http_server, with_get=True) + + @defer.inlineCallbacks + def on_POST(self, request, room_id, event_type): + user = yield self.auth.get_user_by_req(request) + content = _parse_json(request) + + msg_handler = self.handlers.message_handler + event = yield msg_handler.create_and_send_event( + { + "type": event_type, + "content": content, + "room_id": room_id, + "sender": user.to_string(), + } + ) + + defer.returnValue((200, {"event_id": event.event_id})) + + def on_GET(self, request, room_id, event_type, txn_id): + return (200, "Not implemented") + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, event_type, txn_id): + try: + defer.returnValue( + self.txns.get_client_transaction(request, txn_id) + ) + except KeyError: + pass + + response = yield self.on_POST(request, room_id, event_type) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + + +# TODO: Needs unit testing for room ID + alias joins +class JoinRoomAliasServlet(RestServlet): + + def register(self, http_server): + # /join/$room_identifier[/$txn_id] + PATTERN = ("/join/(?P[^/]*)") + register_txn_path(self, PATTERN, http_server) + + @defer.inlineCallbacks + def on_POST(self, request, room_identifier): + user = yield self.auth.get_user_by_req(request) + + # 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. + + identifier = None + is_room_alias = False + try: + identifier = self.hs.parse_roomalias(room_identifier) + is_room_alias = True + except SynapseError: + identifier = self.hs.parse_roomid(room_identifier) + + # TODO: Support for specifying the home server to join with? + + 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 + msg_handler = self.handlers.message_handler + yield msg_handler.create_and_send_event( + { + "type": EventTypes.Member, + "content": {"membership": Membership.JOIN}, + "room_id": identifier.to_string(), + "sender": user.to_string(), + "state_key": user.to_string(), + } + ) + + defer.returnValue((200, {"room_id": identifier.to_string()})) + + @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 +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[^/]*)/members$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + # TODO support Pagination stream API (limit/tokens) + user = yield self.auth.get_user_by_req(request) + handler = self.handlers.room_member_handler + members = yield handler.get_room_members_as_pagination_chunk( + room_id=room_id, + user_id=user.to_string()) + + for event in members["chunk"]: + # FIXME: should probably be state_key here, not user_id + target_user = self.hs.parse_userid(event["user_id"]) + # Presence is an optional cache; don't fail if we can't fetch it + try: + presence_handler = self.handlers.presence_handler + presence_state = yield presence_handler.get_state( + target_user=target_user, auth_user=user + ) + event["content"].update(presence_state) + except: + pass + + defer.returnValue((200, members)) + + +# TODO: Needs unit testing +class RoomMessageListRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P[^/]*)/messages$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + pagination_config = PaginationConfig.from_request( + request, default_limit=10, + ) + with_feedback = "feedback" in request.args + as_client_event = "raw" not in request.args + handler = self.handlers.message_handler + msgs = yield handler.get_messages( + room_id=room_id, + user_id=user.to_string(), + pagin_config=pagination_config, + feedback=with_feedback, + as_client_event=as_client_event + ) + + defer.returnValue((200, msgs)) + + +# TODO: Needs unit testing +class RoomStateRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P[^/]*)/state$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + handler = self.handlers.message_handler + # Get all the current state for this room + events = yield handler.get_state_events( + room_id=room_id, + user_id=user.to_string(), + ) + defer.returnValue((200, events)) + + +# TODO: Needs unit testing +class RoomInitialSyncRestServlet(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P[^/]*)/initialSync$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + user = yield self.auth.get_user_by_req(request) + pagination_config = PaginationConfig.from_request(request) + content = yield self.handlers.message_handler.room_initial_sync( + room_id=room_id, + user_id=user.to_string(), + pagin_config=pagination_config, + ) + defer.returnValue((200, content)) + + +class RoomTriggerBackfill(RestServlet): + PATTERN = client_path_pattern("/rooms/(?P[^/]*)/backfill$") + + @defer.inlineCallbacks + def on_GET(self, request, room_id): + remote_server = urllib.unquote( + request.args["remote"][0] + ).decode("UTF-8") + + limit = int(request.args["limit"][0]) + + handler = self.handlers.federation_handler + events = yield handler.backfill(remote_server, room_id, limit) + + res = [self.hs.serialize_event(event) for event in events] + defer.returnValue((200, res)) + + +# TODO: Needs unit testing +class RoomMembershipRestServlet(RestServlet): + + def register(self, http_server): + # /rooms/$roomid/[invite|join|leave] + PATTERN = ("/rooms/(?P[^/]*)/" + "(?Pjoin|invite|leave|ban|kick)") + register_txn_path(self, PATTERN, http_server) + + @defer.inlineCallbacks + def on_POST(self, request, room_id, membership_action): + user = yield self.auth.get_user_by_req(request) + + content = _parse_json(request) + + # target user is you unless it is an invite + state_key = user.to_string() + if membership_action in ["invite", "ban", "kick"]: + if "user_id" not in content: + raise SynapseError(400, "Missing user_id key.") + state_key = content["user_id"] + + if membership_action == "kick": + membership_action = "leave" + + msg_handler = self.handlers.message_handler + yield msg_handler.create_and_send_event( + { + "type": EventTypes.Member, + "content": {"membership": unicode(membership_action)}, + "room_id": room_id, + "sender": user.to_string(), + "state_key": state_key, + } + ) + + defer.returnValue((200, {})) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, membership_action, txn_id): + try: + defer.returnValue( + self.txns.get_client_transaction(request, txn_id) + ) + except KeyError: + pass + + response = yield self.on_POST(request, room_id, membership_action) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + + +class RoomRedactEventRestServlet(RestServlet): + def register(self, http_server): + PATTERN = ("/rooms/(?P[^/]*)/redact/(?P[^/]*)") + register_txn_path(self, PATTERN, http_server) + + @defer.inlineCallbacks + def on_POST(self, request, room_id, event_id): + user = yield self.auth.get_user_by_req(request) + content = _parse_json(request) + + msg_handler = self.handlers.message_handler + event = yield msg_handler.create_and_send_event( + { + "type": EventTypes.Redaction, + "content": content, + "room_id": room_id, + "sender": user.to_string(), + "redacts": event_id, + } + ) + + defer.returnValue((200, {"event_id": event.event_id})) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, event_id, txn_id): + try: + defer.returnValue( + self.txns.get_client_transaction(request, txn_id) + ) + except KeyError: + pass + + response = yield self.on_POST(request, room_id, event_id) + + self.txns.store_client_transaction(request, txn_id, response) + defer.returnValue(response) + + +class RoomTypingRestServlet(RestServlet): + PATTERN = client_path_pattern( + "/rooms/(?P[^/]*)/typing/(?P[^/]*)$" + ) + + @defer.inlineCallbacks + def on_PUT(self, request, room_id, user_id): + auth_user = yield self.auth.get_user_by_req(request) + + room_id = urllib.unquote(room_id) + target_user = self.hs.parse_userid(urllib.unquote(user_id)) + + content = _parse_json(request) + + typing_handler = self.handlers.typing_notification_handler + + if content["typing"]: + yield typing_handler.started_typing( + target_user=target_user, + auth_user=auth_user, + room_id=room_id, + timeout=content.get("timeout", 30000), + ) + else: + yield typing_handler.stopped_typing( + target_user=target_user, + auth_user=auth_user, + room_id=room_id, + ) + + defer.returnValue((200, {})) + + +def _parse_json(request): + try: + content = json.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.", + errcode=Codes.NOT_JSON) + return content + except ValueError: + raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) + + +def register_txn_path(servlet, regex_string, http_server, with_get=False): + """Registers a transaction-based path. + + This registers two paths: + PUT regex_string/$txnid + POST regex_string + + Args: + regex_string (str): The regex string to register. Must NOT have a + trailing $ as this string will be appended to. + http_server : The http_server to register paths with. + with_get: True to also register respective GET paths for the PUTs. + """ + http_server.register_path( + "POST", + client_path_pattern(regex_string + "$"), + servlet.on_POST + ) + http_server.register_path( + "PUT", + client_path_pattern(regex_string + "/(?P[^/]*)$"), + servlet.on_PUT + ) + if with_get: + http_server.register_path( + "GET", + client_path_pattern(regex_string + "/(?P[^/]*)$"), + servlet.on_GET + ) + + +def register_servlets(hs, http_server): + RoomStateEventRestServlet(hs).register(http_server) + RoomCreateRestServlet(hs).register(http_server) + RoomMemberListRestServlet(hs).register(http_server) + RoomMessageListRestServlet(hs).register(http_server) + JoinRoomAliasServlet(hs).register(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) + RoomRedactEventRestServlet(hs).register(http_server) + RoomTypingRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v1/transactions.py b/synapse/rest/client/v1/transactions.py new file mode 100644 index 0000000000..d933fea18a --- /dev/null +++ b/synapse/rest/client/v1/transactions.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains logic for storing HTTP PUT transactions. This is used +to ensure idempotency when performing PUTs using the REST API.""" +import logging + +logger = logging.getLogger(__name__) + + +# FIXME: elsewhere we use FooStore to indicate something in the storage layer... +class HttpTransactionStore(object): + + def __init__(self): + # { key : (txn_id, response) } + self.transactions = {} + + def get_response(self, key, txn_id): + """Retrieve a response for this request. + + Args: + key (str): A transaction-independent key for this request. Usually + this is a combination of the path (without the transaction id) + and the user's access token. + txn_id (str): The transaction ID for this request + Returns: + A tuple of (HTTP response code, response content) or None. + """ + try: + logger.debug("get_response Key: %s TxnId: %s", key, txn_id) + (last_txn_id, response) = self.transactions[key] + if txn_id == last_txn_id: + logger.info("get_response: Returning a response for %s", key) + return response + except KeyError: + pass + return None + + def store_response(self, key, txn_id, response): + """Stores an HTTP response tuple. + + Args: + key (str): A transaction-independent key for this request. Usually + this is a combination of the path (without the transaction id) + and the user's access token. + txn_id (str): The transaction ID for this request. + response (tuple): A tuple of (HTTP response code, response content) + """ + logger.debug("store_response Key: %s TxnId: %s", key, txn_id) + self.transactions[key] = (txn_id, response) + + def store_client_transaction(self, request, txn_id, response): + """Stores the request/response pair of an HTTP transaction. + + Args: + request (twisted.web.http.Request): The twisted HTTP request. This + request must have the transaction ID as the last path segment. + response (tuple): A tuple of (response code, response dict) + txn_id (str): The transaction ID for this request. + """ + self.store_response(self._get_key(request), txn_id, response) + + def get_client_transaction(self, request, txn_id): + """Retrieves a stored response if there was one. + + Args: + request (twisted.web.http.Request): The twisted HTTP request. This + request must have the transaction ID as the last path segment. + txn_id (str): The transaction ID for this request. + Returns: + The response tuple. + Raises: + KeyError if the transaction was not found. + """ + response = self.get_response(self._get_key(request), txn_id) + if response is None: + raise KeyError("Transaction not found.") + return response + + def _get_key(self, request): + token = request.args["access_token"][0] + path_without_txn_id = request.path.rsplit("/", 1)[0] + return path_without_txn_id + "/" + token diff --git a/synapse/rest/client/v1/voip.py b/synapse/rest/client/v1/voip.py new file mode 100644 index 0000000000..011c35e69b --- /dev/null +++ b/synapse/rest/client/v1/voip.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from base import RestServlet, client_path_pattern + + +import hmac +import hashlib +import base64 + + +class VoipRestServlet(RestServlet): + PATTERN = client_path_pattern("/voip/turnServer$") + + @defer.inlineCallbacks + def on_GET(self, request): + auth_user = yield self.auth.get_user_by_req(request) + + turnUris = self.hs.config.turn_uris + turnSecret = self.hs.config.turn_shared_secret + userLifetime = self.hs.config.turn_user_lifetime + if not turnUris or not turnSecret or not userLifetime: + defer.returnValue((200, {})) + + expiry = (self.hs.get_clock().time_msec() + userLifetime) / 1000 + username = "%d:%s" % (expiry, auth_user.to_string()) + + mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1) + # We need to use standard base64 encoding here, *not* syutil's + # encode_base64 because we need to add the standard padding to get the + # same result as the TURN server. + password = base64.b64encode(mac.digest()) + + defer.returnValue((200, { + 'username': username, + 'password': password, + 'ttl': userLifetime / 1000, + 'uris': turnUris, + })) + + def on_OPTIONS(self, request): + return (200, {}) + + +def register_servlets(hs, http_server): + VoipRestServlet(hs).register(http_server) diff --git a/synapse/rest/media/__init__.py b/synapse/rest/media/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/synapse/rest/media/v0/__init__.py b/synapse/rest/media/v0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/synapse/rest/media/v0/content_repository.py b/synapse/rest/media/v0/content_repository.py new file mode 100644 index 0000000000..79ae0e3d74 --- /dev/null +++ b/synapse/rest/media/v0/content_repository.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.http.server import respond_with_json_bytes + +from synapse.util.stringutils import random_string +from synapse.api.errors import ( + cs_exception, SynapseError, CodeMessageException, Codes, cs_error +) + +from twisted.protocols.basic import FileSender +from twisted.web import server, resource +from twisted.internet import defer + +import base64 +import json +import logging +import os +import re + +logger = logging.getLogger(__name__) + + +class ContentRepoResource(resource.Resource): + """Provides file uploading and downloading. + + Uploads are POSTed to wherever this Resource is linked to. This resource + returns a "content token" which can be used to GET this content again. The + token is typically a path, but it may not be. Tokens can expire, be + one-time uses, etc. + + In this case, the token is a path to the file and contains 3 interesting + sections: + - User ID base64d (for namespacing content to each user) + - random 24 char string + - Content type base64d (so we can return it when clients GET it) + + """ + isLeaf = True + + def __init__(self, hs, directory, auth, external_addr): + resource.Resource.__init__(self) + self.hs = hs + self.directory = directory + self.auth = auth + self.external_addr = external_addr.rstrip('/') + self.max_upload_size = hs.config.max_upload_size + + if not os.path.isdir(self.directory): + os.mkdir(self.directory) + logger.info("ContentRepoResource : Created %s directory.", + self.directory) + + @defer.inlineCallbacks + def map_request_to_name(self, request): + # auth the user + auth_user = yield self.auth.get_user_by_req(request) + + # namespace all file uploads on the user + prefix = base64.urlsafe_b64encode( + auth_user.to_string() + ).replace('=', '') + + # use a random string for the main portion + main_part = random_string(24) + + # suffix with a file extension if we can make one. This is nice to + # provide a hint to clients on the file information. We will also reuse + # this info to spit back the content type to the client. + suffix = "" + if request.requestHeaders.hasHeader("Content-Type"): + content_type = request.requestHeaders.getRawHeaders( + "Content-Type")[0] + suffix = "." + base64.urlsafe_b64encode(content_type) + if (content_type.split("/")[0].lower() in + ["image", "video", "audio"]): + file_ext = content_type.split("/")[-1] + # be a little paranoid and only allow a-z + file_ext = re.sub("[^a-z]", "", file_ext) + suffix += "." + file_ext + + file_name = prefix + main_part + suffix + file_path = os.path.join(self.directory, file_name) + logger.info("User %s is uploading a file to path %s", + auth_user.to_string(), + file_path) + + # keep trying to make a non-clashing file, with a sensible max attempts + attempts = 0 + while os.path.exists(file_path): + main_part = random_string(24) + file_name = prefix + main_part + suffix + file_path = os.path.join(self.directory, file_name) + attempts += 1 + if attempts > 25: # really? Really? + raise SynapseError(500, "Unable to create file.") + + defer.returnValue(file_path) + + def render_GET(self, request): + # no auth here on purpose, to allow anyone to view, even across home + # servers. + + # TODO: A little crude here, we could do this better. + filename = request.path.split('/')[-1] + # be paranoid + filename = re.sub("[^0-9A-z.-_]", "", filename) + + file_path = self.directory + "/" + filename + + logger.debug("Searching for %s", file_path) + + if os.path.isfile(file_path): + # filename has the content type + base64_contentype = filename.split(".")[1] + content_type = base64.urlsafe_b64decode(base64_contentype) + logger.info("Sending file %s", file_path) + f = open(file_path, 'rb') + request.setHeader('Content-Type', content_type) + + # cache for at least a day. + # XXX: we might want to turn this off for data we don't want to + # recommend caching as it's sensitive or private - or at least + # select private. don't bother setting Expires as all our matrix + # clients are smart enough to be happy with Cache-Control (right?) + request.setHeader( + "Cache-Control", "public,max-age=86400,s-maxage=86400" + ) + + d = FileSender().beginFileTransfer(f, request) + + # after the file has been sent, clean up and finish the request + def cbFinished(ignored): + f.close() + request.finish() + d.addCallback(cbFinished) + else: + respond_with_json_bytes( + request, + 404, + json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)), + send_cors=True) + + return server.NOT_DONE_YET + + def render_POST(self, request): + self._async_render(request) + return server.NOT_DONE_YET + + def render_OPTIONS(self, request): + respond_with_json_bytes(request, 200, {}, send_cors=True) + return server.NOT_DONE_YET + + @defer.inlineCallbacks + def _async_render(self, request): + try: + # TODO: The checks here are a bit late. The content will have + # already been uploaded to a tmp file at this point + content_length = request.getHeader("Content-Length") + if content_length is None: + raise SynapseError( + msg="Request must specify a Content-Length", code=400 + ) + if int(content_length) > self.max_upload_size: + raise SynapseError( + msg="Upload request body is too large", + code=413, + ) + + fname = yield self.map_request_to_name(request) + + # TODO I have a suspicious feeling this is just going to block + with open(fname, "wb") as f: + f.write(request.content.read()) + + # FIXME (erikj): These should use constants. + file_name = os.path.basename(fname) + # FIXME: we can't assume what the repo's public mounted path is + # ...plus self-signed SSL won't work to remote clients anyway + # ...and we can't assume that it's SSL anyway, as we might want to + # serve it via the non-SSL listener... + url = "%s/_matrix/content/%s" % ( + self.external_addr, file_name + ) + + respond_with_json_bytes(request, 200, + json.dumps({"content_token": url}), + send_cors=True) + + except CodeMessageException as e: + logger.exception(e) + respond_with_json_bytes(request, e.code, + json.dumps(cs_exception(e))) + except Exception as e: + logger.error("Failed to store file: %s" % e) + respond_with_json_bytes( + request, + 500, + json.dumps({"error": "Internal server error"}), + send_cors=True) diff --git a/synapse/rest/media/v1/__init__.py b/synapse/rest/media/v1/__init__.py new file mode 100644 index 0000000000..d6c6690577 --- /dev/null +++ b/synapse/rest/media/v1/__init__.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import PIL.Image + +# check for JPEG support. +try: + PIL.Image._getdecoder("rgb", "jpeg", None) +except IOError as e: + if str(e).startswith("decoder jpeg not available"): + raise Exception( + "FATAL: jpeg codec not supported. Install pillow correctly! " + " 'sudo apt-get install libjpeg-dev' then 'pip uninstall pillow &&" + " pip install pillow --user'" + ) +except Exception: + # any other exception is fine + pass + + +# check for PNG support. +try: + PIL.Image._getdecoder("rgb", "zip", None) +except IOError as e: + if str(e).startswith("decoder zip not available"): + raise Exception( + "FATAL: zip codec not supported. Install pillow correctly! " + " 'sudo apt-get install libjpeg-dev' then 'pip uninstall pillow &&" + " pip install pillow --user'" + ) +except Exception: + # any other exception is fine + pass diff --git a/synapse/rest/media/v1/base_resource.py b/synapse/rest/media/v1/base_resource.py new file mode 100644 index 0000000000..688e7376ad --- /dev/null +++ b/synapse/rest/media/v1/base_resource.py @@ -0,0 +1,378 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .thumbnailer import Thumbnailer + +from synapse.http.server import respond_with_json +from synapse.util.stringutils import random_string +from synapse.api.errors import ( + cs_exception, CodeMessageException, cs_error, Codes, SynapseError +) + +from twisted.internet import defer +from twisted.web.resource import Resource +from twisted.protocols.basic import FileSender + +import os + +import logging + +logger = logging.getLogger(__name__) + + +class BaseMediaResource(Resource): + isLeaf = True + + def __init__(self, hs, filepaths): + Resource.__init__(self) + self.auth = hs.get_auth() + self.client = hs.get_http_client() + self.clock = hs.get_clock() + self.server_name = hs.hostname + self.store = hs.get_datastore() + self.max_upload_size = hs.config.max_upload_size + self.max_image_pixels = hs.config.max_image_pixels + self.filepaths = filepaths + self.downloads = {} + + @staticmethod + def catch_errors(request_handler): + @defer.inlineCallbacks + def wrapped_request_handler(self, request): + try: + yield request_handler(self, request) + except CodeMessageException as e: + logger.exception(e) + respond_with_json( + request, e.code, cs_exception(e), send_cors=True + ) + except: + logger.exception( + "Failed handle request %s.%s on %r", + request_handler.__module__, + request_handler.__name__, + self, + ) + respond_with_json( + request, + 500, + {"error": "Internal server error"}, + send_cors=True + ) + return wrapped_request_handler + + @staticmethod + def _parse_media_id(request): + try: + server_name, media_id = request.postpath + return (server_name, media_id) + except: + raise SynapseError( + 404, + "Invalid media id token %r" % (request.postpath,), + Codes.UNKKOWN, + ) + + @staticmethod + def _parse_integer(request, arg_name, default=None): + try: + if default is None: + return int(request.args[arg_name][0]) + else: + return int(request.args.get(arg_name, [default])[0]) + except: + raise SynapseError( + 400, + "Missing integer argument %r" % (arg_name,), + Codes.UNKNOWN, + ) + + @staticmethod + def _parse_string(request, arg_name, default=None): + try: + if default is None: + return request.args[arg_name][0] + else: + return request.args.get(arg_name, [default])[0] + except: + raise SynapseError( + 400, + "Missing string argument %r" % (arg_name,), + Codes.UNKNOWN, + ) + + def _respond_404(self, request): + respond_with_json( + request, 404, + cs_error( + "Not found %r" % (request.postpath,), + code=Codes.NOT_FOUND, + ), + send_cors=True + ) + + @staticmethod + def _makedirs(filepath): + dirname = os.path.dirname(filepath) + if not os.path.exists(dirname): + os.makedirs(dirname) + + def _get_remote_media(self, server_name, media_id): + key = (server_name, media_id) + download = self.downloads.get(key) + if download is None: + download = self._get_remote_media_impl(server_name, media_id) + self.downloads[key] = download + + @download.addBoth + def callback(media_info): + del self.downloads[key] + return media_info + return download + + @defer.inlineCallbacks + def _get_remote_media_impl(self, server_name, media_id): + media_info = yield self.store.get_cached_remote_media( + server_name, media_id + ) + if not media_info: + media_info = yield self._download_remote_file( + server_name, media_id + ) + defer.returnValue(media_info) + + @defer.inlineCallbacks + def _download_remote_file(self, server_name, media_id): + file_id = random_string(24) + + fname = self.filepaths.remote_media_filepath( + server_name, file_id + ) + self._makedirs(fname) + + try: + with open(fname, "wb") as f: + request_path = "/".join(( + "/_matrix/media/v1/download", server_name, media_id, + )) + length, headers = yield self.client.get_file( + server_name, request_path, output_stream=f, + max_size=self.max_upload_size, + ) + media_type = headers["Content-Type"][0] + time_now_ms = self.clock.time_msec() + + yield self.store.store_cached_remote_media( + origin=server_name, + media_id=media_id, + media_type=media_type, + time_now_ms=self.clock.time_msec(), + upload_name=None, + media_length=length, + filesystem_id=file_id, + ) + except: + os.remove(fname) + raise + + media_info = { + "media_type": media_type, + "media_length": length, + "upload_name": None, + "created_ts": time_now_ms, + "filesystem_id": file_id, + } + + yield self._generate_remote_thumbnails( + server_name, media_id, media_info + ) + + defer.returnValue(media_info) + + @defer.inlineCallbacks + def _respond_with_file(self, request, media_type, file_path, + file_size=None): + logger.debug("Responding with %r", file_path) + + if os.path.isfile(file_path): + request.setHeader(b"Content-Type", media_type.encode("UTF-8")) + + # cache for at least a day. + # XXX: we might want to turn this off for data we don't want to + # recommend caching as it's sensitive or private - or at least + # select private. don't bother setting Expires as all our + # clients are smart enough to be happy with Cache-Control + request.setHeader( + b"Cache-Control", b"public,max-age=86400,s-maxage=86400" + ) + if file_size is None: + stat = os.stat(file_path) + file_size = stat.st_size + + request.setHeader( + b"Content-Length", b"%d" % (file_size,) + ) + + with open(file_path, "rb") as f: + yield FileSender().beginFileTransfer(f, request) + + request.finish() + else: + self._respond_404(request) + + def _get_thumbnail_requirements(self, media_type): + if media_type == "image/jpeg": + return ( + (32, 32, "crop", "image/jpeg"), + (96, 96, "crop", "image/jpeg"), + (320, 240, "scale", "image/jpeg"), + (640, 480, "scale", "image/jpeg"), + ) + elif (media_type == "image/png") or (media_type == "image/gif"): + return ( + (32, 32, "crop", "image/png"), + (96, 96, "crop", "image/png"), + (320, 240, "scale", "image/png"), + (640, 480, "scale", "image/png"), + ) + else: + return () + + @defer.inlineCallbacks + def _generate_local_thumbnails(self, media_id, media_info): + media_type = media_info["media_type"] + requirements = self._get_thumbnail_requirements(media_type) + if not requirements: + return + + input_path = self.filepaths.local_media_filepath(media_id) + thumbnailer = Thumbnailer(input_path) + m_width = thumbnailer.width + m_height = thumbnailer.height + + if m_width * m_height >= self.max_image_pixels: + logger.info( + "Image too large to thumbnail %r x %r > %r", + m_width, m_height, self.max_image_pixels + ) + return + + scales = set() + crops = set() + for r_width, r_height, r_method, r_type in requirements: + if r_method == "scale": + t_width, t_height = thumbnailer.aspect(r_width, r_height) + scales.add(( + min(m_width, t_width), min(m_height, t_height), r_type, + )) + elif r_method == "crop": + crops.add((r_width, r_height, r_type)) + + for t_width, t_height, t_type in scales: + t_method = "scale" + t_path = self.filepaths.local_media_thumbnail( + media_id, t_width, t_height, t_type, t_method + ) + self._makedirs(t_path) + t_len = thumbnailer.scale(t_path, t_width, t_height, t_type) + yield self.store.store_local_thumbnail( + media_id, t_width, t_height, t_type, t_method, t_len + ) + + for t_width, t_height, t_type in crops: + if (t_width, t_height, t_type) in scales: + # If the aspect ratio of the cropped thumbnail matches a purely + # scaled one then there is no point in calculating a separate + # thumbnail. + continue + t_method = "crop" + t_path = self.filepaths.local_media_thumbnail( + media_id, t_width, t_height, t_type, t_method + ) + self._makedirs(t_path) + t_len = thumbnailer.crop(t_path, t_width, t_height, t_type) + yield self.store.store_local_thumbnail( + media_id, t_width, t_height, t_type, t_method, t_len + ) + + defer.returnValue({ + "width": m_width, + "height": m_height, + }) + + @defer.inlineCallbacks + def _generate_remote_thumbnails(self, server_name, media_id, media_info): + media_type = media_info["media_type"] + file_id = media_info["filesystem_id"] + requirements = self._get_thumbnail_requirements(media_type) + if not requirements: + return + + input_path = self.filepaths.remote_media_filepath(server_name, file_id) + thumbnailer = Thumbnailer(input_path) + m_width = thumbnailer.width + m_height = thumbnailer.height + + if m_width * m_height >= self.max_image_pixels: + logger.info( + "Image too large to thumbnail %r x %r > %r", + m_width, m_height, self.max_image_pixels + ) + return + + scales = set() + crops = set() + for r_width, r_height, r_method, r_type in requirements: + if r_method == "scale": + t_width, t_height = thumbnailer.aspect(r_width, r_height) + scales.add(( + min(m_width, t_width), min(m_height, t_height), r_type, + )) + elif r_method == "crop": + crops.add((r_width, r_height, r_type)) + + for t_width, t_height, t_type in scales: + t_method = "scale" + t_path = self.filepaths.remote_media_thumbnail( + server_name, file_id, t_width, t_height, t_type, t_method + ) + self._makedirs(t_path) + t_len = thumbnailer.scale(t_path, t_width, t_height, t_type) + yield self.store.store_remote_media_thumbnail( + server_name, media_id, file_id, + t_width, t_height, t_type, t_method, t_len + ) + + for t_width, t_height, t_type in crops: + if (t_width, t_height, t_type) in scales: + # If the aspect ratio of the cropped thumbnail matches a purely + # scaled one then there is no point in calculating a separate + # thumbnail. + continue + t_method = "crop" + t_path = self.filepaths.remote_media_thumbnail( + server_name, file_id, t_width, t_height, t_type, t_method + ) + self._makedirs(t_path) + t_len = thumbnailer.crop(t_path, t_width, t_height, t_type) + yield self.store.store_remote_media_thumbnail( + server_name, media_id, file_id, + t_width, t_height, t_type, t_method, t_len + ) + + defer.returnValue({ + "width": m_width, + "height": m_height, + }) diff --git a/synapse/rest/media/v1/download_resource.py b/synapse/rest/media/v1/download_resource.py new file mode 100644 index 0000000000..c585bb11f7 --- /dev/null +++ b/synapse/rest/media/v1/download_resource.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .base_resource import BaseMediaResource + +from twisted.web.server import NOT_DONE_YET +from twisted.internet import defer + +import logging + +logger = logging.getLogger(__name__) + + +class DownloadResource(BaseMediaResource): + def render_GET(self, request): + self._async_render_GET(request) + return NOT_DONE_YET + + @BaseMediaResource.catch_errors + @defer.inlineCallbacks + def _async_render_GET(self, request): + try: + server_name, media_id = request.postpath + except: + self._respond_404(request) + return + + if server_name == self.server_name: + yield self._respond_local_file(request, media_id) + else: + yield self._respond_remote_file(request, server_name, media_id) + + @defer.inlineCallbacks + def _respond_local_file(self, request, media_id): + media_info = yield self.store.get_local_media(media_id) + if not media_info: + self._respond_404(request) + return + + media_type = media_info["media_type"] + media_length = media_info["media_length"] + file_path = self.filepaths.local_media_filepath(media_id) + + yield self._respond_with_file( + request, media_type, file_path, media_length + ) + + @defer.inlineCallbacks + def _respond_remote_file(self, request, server_name, media_id): + media_info = yield self._get_remote_media(server_name, media_id) + + media_type = media_info["media_type"] + media_length = media_info["media_length"] + filesystem_id = media_info["filesystem_id"] + + file_path = self.filepaths.remote_media_filepath( + server_name, filesystem_id + ) + + yield self._respond_with_file( + request, media_type, file_path, media_length + ) diff --git a/synapse/rest/media/v1/filepath.py b/synapse/rest/media/v1/filepath.py new file mode 100644 index 0000000000..ed9a58e9d9 --- /dev/null +++ b/synapse/rest/media/v1/filepath.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + + +class MediaFilePaths(object): + + def __init__(self, base_path): + self.base_path = base_path + + def default_thumbnail(self, default_top_level, default_sub_type, width, + height, content_type, method): + top_level_type, sub_type = content_type.split("/") + file_name = "%i-%i-%s-%s-%s" % ( + width, height, top_level_type, sub_type, method + ) + return os.path.join( + self.base_path, "default_thumbnails", default_top_level, + default_sub_type, file_name + ) + + def local_media_filepath(self, media_id): + return os.path.join( + self.base_path, "local_content", + media_id[0:2], media_id[2:4], media_id[4:] + ) + + def local_media_thumbnail(self, media_id, width, height, content_type, + method): + top_level_type, sub_type = content_type.split("/") + file_name = "%i-%i-%s-%s-%s" % ( + width, height, top_level_type, sub_type, method + ) + return os.path.join( + self.base_path, "local_thumbnails", + media_id[0:2], media_id[2:4], media_id[4:], + file_name + ) + + def remote_media_filepath(self, server_name, file_id): + return os.path.join( + self.base_path, "remote_content", server_name, + file_id[0:2], file_id[2:4], file_id[4:] + ) + + def remote_media_thumbnail(self, server_name, file_id, width, height, + content_type, method): + top_level_type, sub_type = content_type.split("/") + file_name = "%i-%i-%s-%s" % (width, height, top_level_type, sub_type) + return os.path.join( + self.base_path, "remote_thumbnail", server_name, + file_id[0:2], file_id[2:4], file_id[4:], + file_name + ) diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py new file mode 100644 index 0000000000..461cc001f1 --- /dev/null +++ b/synapse/rest/media/v1/media_repository.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .upload_resource import UploadResource +from .download_resource import DownloadResource +from .thumbnail_resource import ThumbnailResource +from .filepath import MediaFilePaths + +from twisted.web.resource import Resource + +import logging + +logger = logging.getLogger(__name__) + + +class MediaRepositoryResource(Resource): + """File uploading and downloading. + + Uploads are POSTed to a resource which returns a token which is used to GET + the download:: + + => POST /_matrix/media/v1/upload HTTP/1.1 + Content-Type: + + + + <= HTTP/1.1 200 OK + Content-Type: application/json + + { "content_uri": "mxc:///" } + + => GET /_matrix/media/v1/download// HTTP/1.1 + + <= HTTP/1.1 200 OK + Content-Type: + Content-Disposition: attachment;filename= + + + + Clients can get thumbnails by supplying a desired width and height and + thumbnailing method:: + + => GET /_matrix/media/v1/thumbnail/ + /?width=&height=&method= HTTP/1.1 + + <= HTTP/1.1 200 OK + Content-Type: image/jpeg or image/png + + + + The thumbnail methods are "crop" and "scale". "scale" trys to return an + image where either the width or the height is smaller than the requested + size. The client should then scale and letterbox the image if it needs to + fit within a given rectangle. "crop" trys to return an image where the + width and height are close to the requested size and the aspect matches + the requested size. The client should scale the image if it needs to fit + within a given rectangle. + """ + + def __init__(self, hs): + Resource.__init__(self) + filepaths = MediaFilePaths(hs.config.media_store_path) + self.putChild("upload", UploadResource(hs, filepaths)) + self.putChild("download", DownloadResource(hs, filepaths)) + self.putChild("thumbnail", ThumbnailResource(hs, filepaths)) diff --git a/synapse/rest/media/v1/thumbnail_resource.py b/synapse/rest/media/v1/thumbnail_resource.py new file mode 100644 index 0000000000..84f5e3463c --- /dev/null +++ b/synapse/rest/media/v1/thumbnail_resource.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from .base_resource import BaseMediaResource + +from twisted.web.server import NOT_DONE_YET +from twisted.internet import defer + +import logging + +logger = logging.getLogger(__name__) + + +class ThumbnailResource(BaseMediaResource): + isLeaf = True + + def render_GET(self, request): + self._async_render_GET(request) + return NOT_DONE_YET + + @BaseMediaResource.catch_errors + @defer.inlineCallbacks + def _async_render_GET(self, request): + server_name, media_id = self._parse_media_id(request) + width = self._parse_integer(request, "width") + height = self._parse_integer(request, "height") + method = self._parse_string(request, "method", "scale") + m_type = self._parse_string(request, "type", "image/png") + + if server_name == self.server_name: + yield self._respond_local_thumbnail( + request, media_id, width, height, method, m_type + ) + else: + yield self._respond_remote_thumbnail( + request, server_name, media_id, + width, height, method, m_type + ) + + @defer.inlineCallbacks + def _respond_local_thumbnail(self, request, media_id, width, height, + method, m_type): + media_info = yield self.store.get_local_media(media_id) + + if not media_info: + self._respond_404(request) + return + + thumbnail_infos = yield self.store.get_local_media_thumbnails(media_id) + + if thumbnail_infos: + thumbnail_info = self._select_thumbnail( + width, height, method, m_type, thumbnail_infos + ) + t_width = thumbnail_info["thumbnail_width"] + t_height = thumbnail_info["thumbnail_height"] + t_type = thumbnail_info["thumbnail_type"] + t_method = thumbnail_info["thumbnail_method"] + + file_path = self.filepaths.local_media_thumbnail( + media_id, t_width, t_height, t_type, t_method, + ) + yield self._respond_with_file(request, t_type, file_path) + + else: + yield self._respond_default_thumbnail( + request, media_info, width, height, method, m_type, + ) + + @defer.inlineCallbacks + def _respond_remote_thumbnail(self, request, server_name, media_id, width, + height, method, m_type): + # TODO: Don't download the whole remote file + # We should proxy the thumbnail from the remote server instead. + media_info = yield self._get_remote_media(server_name, media_id) + + thumbnail_infos = yield self.store.get_remote_media_thumbnails( + server_name, media_id, + ) + + if thumbnail_infos: + thumbnail_info = self._select_thumbnail( + width, height, method, m_type, thumbnail_infos + ) + t_width = thumbnail_info["thumbnail_width"] + t_height = thumbnail_info["thumbnail_height"] + t_type = thumbnail_info["thumbnail_type"] + t_method = thumbnail_info["thumbnail_method"] + file_id = thumbnail_info["filesystem_id"] + t_length = thumbnail_info["thumbnail_length"] + + file_path = self.filepaths.remote_media_thumbnail( + server_name, file_id, t_width, t_height, t_type, t_method, + ) + yield self._respond_with_file(request, t_type, file_path, t_length) + else: + yield self._respond_default_thumbnail( + request, media_info, width, height, method, m_type, + ) + + @defer.inlineCallbacks + def _respond_default_thumbnail(self, request, media_info, width, height, + method, m_type): + media_type = media_info["media_type"] + top_level_type = media_type.split("/")[0] + sub_type = media_type.split("/")[-1].split(";")[0] + thumbnail_infos = yield self.store.get_default_thumbnails( + top_level_type, sub_type, + ) + if not thumbnail_infos: + thumbnail_infos = yield self.store.get_default_thumbnails( + top_level_type, "_default", + ) + if not thumbnail_infos: + thumbnail_infos = yield self.store.get_default_thumbnails( + "_default", "_default", + ) + if not thumbnail_infos: + self._respond_404(request) + return + + thumbnail_info = self._select_thumbnail( + width, height, "crop", m_type, thumbnail_infos + ) + + t_width = thumbnail_info["thumbnail_width"] + t_height = thumbnail_info["thumbnail_height"] + t_type = thumbnail_info["thumbnail_type"] + t_method = thumbnail_info["thumbnail_method"] + t_length = thumbnail_info["thumbnail_length"] + + file_path = self.filepaths.default_thumbnail( + top_level_type, sub_type, t_width, t_height, t_type, t_method, + ) + yield self.respond_with_file(request, t_type, file_path, t_length) + + def _select_thumbnail(self, desired_width, desired_height, desired_method, + desired_type, thumbnail_infos): + d_w = desired_width + d_h = desired_height + + if desired_method.lower() == "crop": + info_list = [] + for info in thumbnail_infos: + t_w = info["thumbnail_width"] + t_h = info["thumbnail_height"] + t_method = info["thumbnail_method"] + if t_method == "scale" or t_method == "crop": + aspect_quality = abs(d_w * t_h - d_h * t_w) + size_quality = abs((d_w - t_w) * (d_h - t_h)) + type_quality = desired_type != info["thumbnail_type"] + length_quality = info["thumbnail_length"] + info_list.append(( + aspect_quality, size_quality, type_quality, + length_quality, info + )) + if info_list: + return min(info_list)[-1] + else: + info_list = [] + info_list2 = [] + for info in thumbnail_infos: + t_w = info["thumbnail_width"] + t_h = info["thumbnail_height"] + t_method = info["thumbnail_method"] + size_quality = abs((d_w - t_w) * (d_h - t_h)) + type_quality = desired_type != info["thumbnail_type"] + length_quality = info["thumbnail_length"] + if t_method == "scale" and (t_w >= d_w or t_h >= d_h): + info_list.append(( + size_quality, type_quality, length_quality, info + )) + elif t_method == "scale": + info_list2.append(( + size_quality, type_quality, length_quality, info + )) + if info_list: + return min(info_list)[-1] + else: + return min(info_list2)[-1] diff --git a/synapse/rest/media/v1/thumbnailer.py b/synapse/rest/media/v1/thumbnailer.py new file mode 100644 index 0000000000..28404f2b7b --- /dev/null +++ b/synapse/rest/media/v1/thumbnailer.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import PIL.Image as Image +from io import BytesIO + + +class Thumbnailer(object): + + FORMATS = { + "image/jpeg": "JPEG", + "image/png": "PNG", + } + + def __init__(self, input_path): + self.image = Image.open(input_path) + self.width, self.height = self.image.size + + def aspect(self, max_width, max_height): + """Calculate the largest size that preserves aspect ratio which + fits within the given rectangle:: + + (w_in / h_in) = (w_out / h_out) + w_out = min(w_max, h_max * (w_in / h_in)) + h_out = min(h_max, w_max * (h_in / w_in)) + + Args: + max_width: The largest possible width. + max_height: The larget possible height. + """ + + if max_width * self.height < max_height * self.width: + return (max_width, (max_width * self.height) // self.width) + else: + return ((max_height * self.width) // self.height, max_height) + + def scale(self, output_path, width, height, output_type): + """Rescales the image to the given dimensions""" + scaled = self.image.resize((width, height), Image.ANTIALIAS) + return self.save_image(scaled, output_type, output_path) + + def crop(self, output_path, width, height, output_type): + """Rescales and crops the image to the given dimensions preserving + aspect:: + (w_in / h_in) = (w_scaled / h_scaled) + w_scaled = max(w_out, h_out * (w_in / h_in)) + h_scaled = max(h_out, w_out * (h_in / w_in)) + + Args: + max_width: The largest possible width. + max_height: The larget possible height. + """ + if width * self.height > height * self.width: + scaled_height = (width * self.height) // self.width + scaled_image = self.image.resize( + (width, scaled_height), Image.ANTIALIAS + ) + crop_top = (scaled_height - height) // 2 + crop_bottom = height + crop_top + cropped = scaled_image.crop((0, crop_top, width, crop_bottom)) + else: + scaled_width = (height * self.width) // self.height + scaled_image = self.image.resize( + (scaled_width, height), Image.ANTIALIAS + ) + crop_left = (scaled_width - width) // 2 + crop_right = width + crop_left + cropped = scaled_image.crop((crop_left, 0, crop_right, height)) + return self.save_image(cropped, output_type, output_path) + + def save_image(self, output_image, output_type, output_path): + output_bytes_io = BytesIO() + output_image.save(output_bytes_io, self.FORMATS[output_type], quality=70) + output_bytes = output_bytes_io.getvalue() + with open(output_path, "wb") as output_file: + output_file.write(output_bytes) + return len(output_bytes) diff --git a/synapse/rest/media/v1/upload_resource.py b/synapse/rest/media/v1/upload_resource.py new file mode 100644 index 0000000000..b1718a630b --- /dev/null +++ b/synapse/rest/media/v1/upload_resource.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.http.server import respond_with_json + +from synapse.util.stringutils import random_string +from synapse.api.errors import ( + cs_exception, SynapseError, CodeMessageException +) + +from twisted.web.server import NOT_DONE_YET +from twisted.internet import defer + +from .base_resource import BaseMediaResource + +import logging + +logger = logging.getLogger(__name__) + + +class UploadResource(BaseMediaResource): + def render_POST(self, request): + self._async_render_POST(request) + return NOT_DONE_YET + + def render_OPTIONS(self, request): + respond_with_json(request, 200, {}, send_cors=True) + return NOT_DONE_YET + + @defer.inlineCallbacks + def _async_render_POST(self, request): + try: + auth_user = yield self.auth.get_user_by_req(request) + # TODO: The checks here are a bit late. The content will have + # already been uploaded to a tmp file at this point + content_length = request.getHeader("Content-Length") + if content_length is None: + raise SynapseError( + msg="Request must specify a Content-Length", code=400 + ) + if int(content_length) > self.max_upload_size: + raise SynapseError( + msg="Upload request body is too large", + code=413, + ) + + headers = request.requestHeaders + + if headers.hasHeader("Content-Type"): + media_type = headers.getRawHeaders("Content-Type")[0] + else: + raise SynapseError( + msg="Upload request missing 'Content-Type'", + code=400, + ) + + #if headers.hasHeader("Content-Disposition"): + # disposition = headers.getRawHeaders("Content-Disposition")[0] + # TODO(markjh): parse content-dispostion + + media_id = random_string(24) + + fname = self.filepaths.local_media_filepath(media_id) + self._makedirs(fname) + + # This shouldn't block for very long because the content will have + # already been uploaded at this point. + with open(fname, "wb") as f: + f.write(request.content.read()) + + yield self.store.store_local_media( + media_id=media_id, + media_type=media_type, + time_now_ms=self.clock.time_msec(), + upload_name=None, + media_length=content_length, + user_id=auth_user, + ) + media_info = { + "media_type": media_type, + "media_length": content_length, + } + + yield self._generate_local_thumbnails(media_id, media_info) + + content_uri = "mxc://%s/%s" % (self.server_name, media_id) + + respond_with_json( + request, 200, {"content_uri": content_uri}, send_cors=True + ) + except CodeMessageException as e: + logger.exception(e) + respond_with_json(request, e.code, cs_exception(e), send_cors=True) + except: + logger.exception("Failed to store file") + respond_with_json( + request, + 500, + {"error": "Internal server error"}, + send_cors=True + ) diff --git a/synapse/server.py b/synapse/server.py index 57a95bf753..e9add8e2b4 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -24,7 +24,7 @@ from synapse.events.utils import serialize_event from synapse.notifier import Notifier from synapse.api.auth import Auth from synapse.handlers import Handlers -from synapse.client.v1 import RestServletFactory +from synapse.rest.client.v1 import RestServletFactory from synapse.state import StateHandler from synapse.storage import DataStore from synapse.types import UserID, RoomAlias, RoomID, EventID diff --git a/tests/client/v1/test_events.py b/tests/client/v1/test_events.py index 9b36dd3225..e914b05a52 100644 --- a/tests/client/v1/test_events.py +++ b/tests/client/v1/test_events.py @@ -19,9 +19,9 @@ from tests import unittest # twisted imports from twisted.internet import defer -import synapse.client.v1.events -import synapse.client.v1.register -import synapse.client.v1.room +import synapse.rest.client.v1.events +import synapse.rest.client.v1.register +import synapse.rest.client.v1.room from synapse.server import HomeServer @@ -144,9 +144,9 @@ class EventStreamPermissionsTestCase(RestTestCase): hs.get_clock().time_msec.return_value = 1000000 hs.get_clock().time.return_value = 1000 - synapse.client.v1.register.register_servlets(hs, self.mock_resource) - synapse.client.v1.events.register_servlets(hs, self.mock_resource) - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.register.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.events.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) # register an account self.user_id = "sid1" diff --git a/tests/client/v1/test_rooms.py b/tests/client/v1/test_rooms.py index 33a8631d76..4d529ef007 100644 --- a/tests/client/v1/test_rooms.py +++ b/tests/client/v1/test_rooms.py @@ -18,7 +18,7 @@ # twisted imports from twisted.internet import defer -import synapse.client.v1.room +import synapse.rest.client.v1.room from synapse.api.constants import Membership from synapse.server import HomeServer @@ -82,7 +82,7 @@ class RoomPermissionsTestCase(RestTestCase): self.auth_user_id = self.rmcreator_id - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) self.auth = hs.get_auth() @@ -476,7 +476,7 @@ class RoomsMemberListTestCase(RestTestCase): return defer.succeed(None) hs.get_datastore().insert_client_ip = _insert_client_ip - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) def tearDown(self): pass @@ -565,7 +565,7 @@ class RoomsCreateTestCase(RestTestCase): return defer.succeed(None) hs.get_datastore().insert_client_ip = _insert_client_ip - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) def tearDown(self): pass @@ -668,7 +668,7 @@ class RoomTopicTestCase(RestTestCase): return defer.succeed(None) hs.get_datastore().insert_client_ip = _insert_client_ip - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) # create the room self.room_id = yield self.create_room_as(self.user_id) @@ -783,7 +783,7 @@ class RoomMemberStateTestCase(RestTestCase): return defer.succeed(None) hs.get_datastore().insert_client_ip = _insert_client_ip - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) self.room_id = yield self.create_room_as(self.user_id) @@ -919,7 +919,7 @@ class RoomMessagesTestCase(RestTestCase): return defer.succeed(None) hs.get_datastore().insert_client_ip = _insert_client_ip - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) self.room_id = yield self.create_room_as(self.user_id) @@ -1023,7 +1023,7 @@ class RoomInitialSyncTestCase(RestTestCase): return defer.succeed(None) hs.get_datastore().insert_client_ip = _insert_client_ip - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) # Since I'm getting my own presence I need to exist as far as presence # is concerned. diff --git a/tests/client/v1/test_typing.py b/tests/client/v1/test_typing.py index d6d677bde3..af3a9a6c1c 100644 --- a/tests/client/v1/test_typing.py +++ b/tests/client/v1/test_typing.py @@ -18,7 +18,7 @@ # twisted imports from twisted.internet import defer -import synapse.client.v1.room +import synapse.rest.client.v1.room from synapse.server import HomeServer from ...utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey @@ -104,7 +104,7 @@ class RoomTypingTestCase(RestTestCase): hs.get_handlers().room_member_handler.fetch_room_distributions_into = ( fetch_room_distributions_into) - synapse.client.v1.room.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) self.room_id = yield self.create_room_as(self.user_id) # Need another user to make notifications actually work -- cgit 1.5.1 From 53584420a50d09b40f3235d8e4c033009e8eb0da Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 22 Jan 2015 16:13:27 +0000 Subject: Move client rest tests back under rest --- tests/client/__init__.py | 14 - tests/client/v1/__init__.py | 15 - tests/client/v1/test_events.py | 225 ------- tests/client/v1/test_presence.py | 372 ------------ tests/client/v1/test_profile.py | 150 ----- tests/client/v1/test_rooms.py | 1068 --------------------------------- tests/client/v1/test_typing.py | 164 ----- tests/client/v1/utils.py | 134 ----- tests/rest/__init__.py | 14 + tests/rest/client/__init__.py | 14 + tests/rest/client/v1/__init__.py | 15 + tests/rest/client/v1/test_events.py | 225 +++++++ tests/rest/client/v1/test_presence.py | 372 ++++++++++++ tests/rest/client/v1/test_profile.py | 150 +++++ tests/rest/client/v1/test_rooms.py | 1068 +++++++++++++++++++++++++++++++++ tests/rest/client/v1/test_typing.py | 164 +++++ tests/rest/client/v1/utils.py | 134 +++++ 17 files changed, 2156 insertions(+), 2142 deletions(-) delete mode 100644 tests/client/__init__.py delete mode 100644 tests/client/v1/__init__.py delete mode 100644 tests/client/v1/test_events.py delete mode 100644 tests/client/v1/test_presence.py delete mode 100644 tests/client/v1/test_profile.py delete mode 100644 tests/client/v1/test_rooms.py delete mode 100644 tests/client/v1/test_typing.py delete mode 100644 tests/client/v1/utils.py create mode 100644 tests/rest/__init__.py create mode 100644 tests/rest/client/__init__.py create mode 100644 tests/rest/client/v1/__init__.py create mode 100644 tests/rest/client/v1/test_events.py create mode 100644 tests/rest/client/v1/test_presence.py create mode 100644 tests/rest/client/v1/test_profile.py create mode 100644 tests/rest/client/v1/test_rooms.py create mode 100644 tests/rest/client/v1/test_typing.py create mode 100644 tests/rest/client/v1/utils.py diff --git a/tests/client/__init__.py b/tests/client/__init__.py deleted file mode 100644 index 1a84d94cd9..0000000000 --- a/tests/client/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2015 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/tests/client/v1/__init__.py b/tests/client/v1/__init__.py deleted file mode 100644 index 9bff9ec169..0000000000 --- a/tests/client/v1/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - diff --git a/tests/client/v1/test_events.py b/tests/client/v1/test_events.py deleted file mode 100644 index e914b05a52..0000000000 --- a/tests/client/v1/test_events.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" Tests REST events for /events paths.""" -from tests import unittest - -# twisted imports -from twisted.internet import defer - -import synapse.rest.client.v1.events -import synapse.rest.client.v1.register -import synapse.rest.client.v1.room - -from synapse.server import HomeServer - -from ...utils import MockHttpResource, SQLiteMemoryDbPool, MockKey -from .utils import RestTestCase - -from mock import Mock, NonCallableMock - - -PATH_PREFIX = "/_matrix/client/api/v1" - - -class EventStreamPaginationApiTestCase(unittest.TestCase): - """ Tests event streaming query parameters and start/end keys used in the - Pagination stream API. """ - user_id = "sid1" - - def setUp(self): - # configure stream and inject items - pass - - def tearDown(self): - pass - - def TODO_test_long_poll(self): - # stream from 'end' key, send (self+other) message, expect message. - - # stream from 'END', send (self+other) message, expect message. - - # stream from 'end' key, send (self+other) topic, expect topic. - - # stream from 'END', send (self+other) topic, expect topic. - - # stream from 'end' key, send (self+other) invite, expect invite. - - # stream from 'END', send (self+other) invite, expect invite. - - pass - - def TODO_test_stream_forward(self): - # stream from START, expect injected items - - # stream from 'start' key, expect same content - - # stream from 'end' key, expect nothing - - # stream from 'END', expect nothing - - # The following is needed for cases where content is removed e.g. you - # left a room, so the token you're streaming from is > the one that - # would be returned naturally from START>END. - # stream from very new token (higher than end key), expect same token - # returned as end key - pass - - def TODO_test_limits(self): - # stream from a key, expect limit_num items - - # stream from START, expect limit_num items - - pass - - def TODO_test_range(self): - # stream from key to key, expect X items - - # stream from key to END, expect X items - - # stream from START to key, expect X items - - # stream from START to END, expect all items - pass - - def TODO_test_direction(self): - # stream from END to START and fwds, expect newest first - - # stream from END to START and bwds, expect oldest first - - # stream from START to END and fwds, expect oldest first - - # stream from START to END and bwds, expect newest first - - pass - - -class EventStreamPermissionsTestCase(RestTestCase): - """ Tests event streaming (GET /events). """ - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "test", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - clock=Mock(spec=[ - "call_later", - "cancel_call_later", - "time_msec", - "time" - ]), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - hs.config.enable_registration_captcha = False - - hs.get_handlers().federation_handler = Mock() - - hs.get_clock().time_msec.return_value = 1000000 - hs.get_clock().time.return_value = 1000 - - synapse.rest.client.v1.register.register_servlets(hs, self.mock_resource) - synapse.rest.client.v1.events.register_servlets(hs, self.mock_resource) - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - # register an account - self.user_id = "sid1" - response = yield self.register(self.user_id) - self.token = response["access_token"] - self.user_id = response["user_id"] - - # register a 2nd account - self.other_user = "other1" - response = yield self.register(self.other_user) - self.other_token = response["access_token"] - self.other_user = response["user_id"] - - def tearDown(self): - pass - - @defer.inlineCallbacks - def test_stream_basic_permissions(self): - # invalid token, expect 403 - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s" % ("invalid" + self.token, ) - ) - self.assertEquals(403, code, msg=str(response)) - - # valid token, expect content - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s&timeout=0" % (self.token,) - ) - self.assertEquals(200, code, msg=str(response)) - self.assertTrue("chunk" in response) - self.assertTrue("start" in response) - self.assertTrue("end" in response) - - @defer.inlineCallbacks - def test_stream_room_permissions(self): - 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) - yield self.invite( - room_id, - src=self.other_user, - targ=self.user_id, - tok=self.other_token - ) - - (code, response) = yield self.mock_resource.trigger_get( - "/events?access_token=%s&timeout=0" % (self.token,) - ) - self.assertEquals(200, code, msg=str(response)) - - self.assertEquals(0, len(response["chunk"])) - - # joined room (expect all content for room) - yield self.join(room=room_id, user=self.user_id, tok=self.token) - - # left to room (expect no content for room) - - def TODO_test_stream_items(self): - # new user, no content - - # join room, expect 1 item (join) - - # send message, expect 2 items (join,send) - - # set topic, expect 3 items (join,send,topic) - - # someone else join room, expect 4 (join,send,topic,join) - - # someone else send message, expect 5 (join,send.topic,join,send) - - # someone else set topic, expect 6 (join,send,topic,join,send,topic) - pass diff --git a/tests/client/v1/test_presence.py b/tests/client/v1/test_presence.py deleted file mode 100644 index e7d636c74d..0000000000 --- a/tests/client/v1/test_presence.py +++ /dev/null @@ -1,372 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /presence paths.""" - -from tests import unittest -from twisted.internet import defer - -from mock import Mock - -from ...utils import MockHttpResource, MockKey - -from synapse.api.constants import PresenceState -from synapse.handlers.presence import PresenceHandler -from synapse.server import HomeServer - - -OFFLINE = PresenceState.OFFLINE -UNAVAILABLE = PresenceState.UNAVAILABLE -ONLINE = PresenceState.ONLINE - - -myid = "@apple:test" -PATH_PREFIX = "/_matrix/client/api/v1" - - -class JustPresenceHandlers(object): - def __init__(self, hs): - self.presence_handler = PresenceHandler(hs) - - -class PresenceStateTestCase(unittest.TestCase): - - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.mock_config = Mock() - self.mock_config.signing_key = [MockKey()] - hs = HomeServer("test", - db_pool=None, - datastore=Mock(spec=[ - "get_presence_state", - "set_presence_state", - "insert_client_ip", - ]), - http_client=None, - resource_for_client=self.mock_resource, - resource_for_federation=self.mock_resource, - config=self.mock_config, - ) - hs.handlers = JustPresenceHandlers(hs) - - self.datastore = hs.get_datastore() - - def get_presence_list(*a, **kw): - return defer.succeed([]) - self.datastore.get_presence_list = get_presence_list - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(myid), - "admin": False, - "device_id": None, - } - - hs.get_auth().get_user_by_token = _get_user_by_token - - room_member_handler = hs.handlers.room_member_handler = Mock( - spec=[ - "get_rooms_for_user", - ] - ) - - def get_rooms_for_user(user): - return defer.succeed([]) - room_member_handler.get_rooms_for_user = get_rooms_for_user - - hs.register_servlets() - - self.u_apple = hs.parse_userid(myid) - - @defer.inlineCallbacks - def test_get_my_status(self): - mocked_get = self.datastore.get_presence_state - mocked_get.return_value = defer.succeed( - {"state": ONLINE, "status_msg": "Available"} - ) - - (code, response) = yield self.mock_resource.trigger("GET", - "/presence/%s/status" % (myid), None) - - self.assertEquals(200, code) - self.assertEquals( - {"presence": ONLINE, "status_msg": "Available"}, - response - ) - mocked_get.assert_called_with("apple") - - @defer.inlineCallbacks - def test_set_my_status(self): - mocked_set = self.datastore.set_presence_state - mocked_set.return_value = defer.succeed({"state": OFFLINE}) - - (code, response) = yield self.mock_resource.trigger("PUT", - "/presence/%s/status" % (myid), - '{"presence": "unavailable", "status_msg": "Away"}') - - self.assertEquals(200, code) - mocked_set.assert_called_with("apple", - {"state": UNAVAILABLE, "status_msg": "Away"} - ) - - -class PresenceListTestCase(unittest.TestCase): - - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.mock_config = Mock() - self.mock_config.signing_key = [MockKey()] - - hs = HomeServer("test", - db_pool=None, - datastore=Mock(spec=[ - "has_presence_state", - "get_presence_state", - "allow_presence_visible", - "is_presence_visible", - "add_presence_list_pending", - "set_presence_list_accepted", - "del_presence_list", - "get_presence_list", - "insert_client_ip", - ]), - http_client=None, - resource_for_client=self.mock_resource, - resource_for_federation=self.mock_resource, - config=self.mock_config, - ) - hs.handlers = JustPresenceHandlers(hs) - - self.datastore = hs.get_datastore() - - def has_presence_state(user_localpart): - return defer.succeed( - user_localpart in ("apple", "banana",) - ) - self.datastore.has_presence_state = has_presence_state - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(myid), - "admin": False, - "device_id": None, - } - - room_member_handler = hs.handlers.room_member_handler = Mock( - spec=[ - "get_rooms_for_user", - ] - ) - - hs.get_auth().get_user_by_token = _get_user_by_token - - hs.register_servlets() - - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - - @defer.inlineCallbacks - def test_get_my_list(self): - self.datastore.get_presence_list.return_value = defer.succeed( - [{"observed_user_id": "@banana:test"}], - ) - - (code, response) = yield self.mock_resource.trigger("GET", - "/presence/list/%s" % (myid), None) - - self.assertEquals(200, code) - self.assertEquals([ - {"user_id": "@banana:test", "presence": OFFLINE}, - ], response) - - self.datastore.get_presence_list.assert_called_with( - "apple", accepted=True - ) - - @defer.inlineCallbacks - def test_invite(self): - self.datastore.add_presence_list_pending.return_value = ( - defer.succeed(()) - ) - self.datastore.is_presence_visible.return_value = defer.succeed( - True - ) - - (code, response) = yield self.mock_resource.trigger("POST", - "/presence/list/%s" % (myid), - """{"invite": ["@banana:test"]}""" - ) - - self.assertEquals(200, code) - - self.datastore.add_presence_list_pending.assert_called_with( - "apple", "@banana:test" - ) - self.datastore.set_presence_list_accepted.assert_called_with( - "apple", "@banana:test" - ) - - @defer.inlineCallbacks - def test_drop(self): - self.datastore.del_presence_list.return_value = ( - defer.succeed(()) - ) - - (code, response) = yield self.mock_resource.trigger("POST", - "/presence/list/%s" % (myid), - """{"drop": ["@banana:test"]}""" - ) - - self.assertEquals(200, code) - - self.datastore.del_presence_list.assert_called_with( - "apple", "@banana:test" - ) - - -class PresenceEventStreamTestCase(unittest.TestCase): - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - self.mock_config = Mock() - self.mock_config.signing_key = [MockKey()] - - # HIDEOUS HACKERY - # TODO(paul): This should be injected in via the HomeServer DI system - from synapse.streams.events import ( - PresenceEventSource, NullSource, EventSources - ) - - old_SOURCE_TYPES = EventSources.SOURCE_TYPES - def tearDown(): - EventSources.SOURCE_TYPES = old_SOURCE_TYPES - self.tearDown = tearDown - - EventSources.SOURCE_TYPES = { - k: NullSource for k in old_SOURCE_TYPES.keys() - } - EventSources.SOURCE_TYPES["presence"] = PresenceEventSource - - hs = HomeServer("test", - db_pool=None, - http_client=None, - resource_for_client=self.mock_resource, - resource_for_federation=self.mock_resource, - datastore=Mock(spec=[ - "set_presence_state", - "get_presence_list", - ]), - clock=Mock(spec=[ - "call_later", - "cancel_call_later", - "time_msec", - ]), - config=self.mock_config, - ) - - hs.get_clock().time_msec.return_value = 1000000 - - def _get_user_by_req(req=None): - return hs.parse_userid(myid) - - hs.get_auth().get_user_by_req = _get_user_by_req - - hs.register_servlets() - - hs.handlers.room_member_handler = Mock(spec=[]) - - self.room_members = [] - - def get_rooms_for_user(user): - if user in self.room_members: - return ["a-room"] - else: - return [] - hs.handlers.room_member_handler.get_rooms_for_user = get_rooms_for_user - - self.mock_datastore = hs.get_datastore() - - def get_profile_displayname(user_id): - return defer.succeed("Frank") - self.mock_datastore.get_profile_displayname = get_profile_displayname - - def get_profile_avatar_url(user_id): - return defer.succeed(None) - self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url - - def user_rooms_intersect(user_list): - room_member_ids = map(lambda u: u.to_string(), self.room_members) - - shared = all(map(lambda i: i in room_member_ids, user_list)) - return defer.succeed(shared) - self.mock_datastore.user_rooms_intersect = user_rooms_intersect - - def get_joined_hosts_for_room(room_id): - return [] - self.mock_datastore.get_joined_hosts_for_room = get_joined_hosts_for_room - - self.presence = hs.get_handlers().presence_handler - - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - - @defer.inlineCallbacks - def test_shortpoll(self): - self.room_members = [self.u_apple, self.u_banana] - - self.mock_datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE} - ) - self.mock_datastore.get_presence_list.return_value = defer.succeed( - [] - ) - - (code, response) = yield self.mock_resource.trigger("GET", - "/events?timeout=0", None) - - self.assertEquals(200, code) - - # We've forced there to be only one data stream so the tokens will - # all be ours - - # I'll already get my own presence state change - self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []}, - response - ) - - self.mock_datastore.set_presence_state.return_value = defer.succeed( - {"state": ONLINE} - ) - self.mock_datastore.get_presence_list.return_value = defer.succeed( - [] - ) - - yield self.presence.set_state(self.u_banana, self.u_banana, - state={"presence": ONLINE} - ) - - (code, response) = yield self.mock_resource.trigger("GET", - "/events?from=0_1_0&timeout=0", None) - - self.assertEquals(200, code) - self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [ - {"type": "m.presence", - "content": { - "user_id": "@banana:test", - "presence": ONLINE, - "displayname": "Frank", - "last_active_ago": 0, - }}, - ]}, response) diff --git a/tests/client/v1/test_profile.py b/tests/client/v1/test_profile.py deleted file mode 100644 index 1182cc54eb..0000000000 --- a/tests/client/v1/test_profile.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /profile paths.""" - -from tests import unittest -from twisted.internet import defer - -from mock import Mock, NonCallableMock - -from ...utils import MockHttpResource, MockKey - -from synapse.api.errors import SynapseError, AuthError -from synapse.server import HomeServer - -myid = "@1234ABCD:test" -PATH_PREFIX = "/_matrix/client/api/v1" - - -class ProfileTestCase(unittest.TestCase): - """ Tests profile management. """ - - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.mock_handler = Mock(spec=[ - "get_displayname", - "set_displayname", - "get_avatar_url", - "set_avatar_url", - ]) - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - hs = HomeServer("test", - db_pool=None, - http_client=None, - resource_for_client=self.mock_resource, - federation=Mock(), - replication_layer=Mock(), - datastore=None, - config=self.mock_config, - ) - - def _get_user_by_req(request=None): - return hs.parse_userid(myid) - - hs.get_auth().get_user_by_req = _get_user_by_req - - hs.get_handlers().profile_handler = self.mock_handler - - hs.register_servlets() - - @defer.inlineCallbacks - def test_get_my_name(self): - mocked_get = self.mock_handler.get_displayname - mocked_get.return_value = defer.succeed("Frank") - - (code, response) = yield self.mock_resource.trigger("GET", - "/profile/%s/displayname" % (myid), None) - - self.assertEquals(200, code) - self.assertEquals({"displayname": "Frank"}, response) - self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD") - - @defer.inlineCallbacks - def test_set_my_name(self): - mocked_set = self.mock_handler.set_displayname - mocked_set.return_value = defer.succeed(()) - - (code, response) = yield self.mock_resource.trigger("PUT", - "/profile/%s/displayname" % (myid), - '{"displayname": "Frank Jr."}') - - self.assertEquals(200, code) - self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD") - self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD") - self.assertEquals(mocked_set.call_args[0][2], "Frank Jr.") - - @defer.inlineCallbacks - def test_set_my_name_noauth(self): - mocked_set = self.mock_handler.set_displayname - mocked_set.side_effect = AuthError(400, "message") - - (code, response) = yield self.mock_resource.trigger("PUT", - "/profile/%s/displayname" % ("@4567:test"), '"Frank Jr."') - - self.assertTrue(400 <= code < 499, - msg="code %d is in the 4xx range" % (code)) - - @defer.inlineCallbacks - def test_get_other_name(self): - mocked_get = self.mock_handler.get_displayname - mocked_get.return_value = defer.succeed("Bob") - - (code, response) = yield self.mock_resource.trigger("GET", - "/profile/%s/displayname" % ("@opaque:elsewhere"), None) - - self.assertEquals(200, code) - self.assertEquals({"displayname": "Bob"}, response) - - @defer.inlineCallbacks - def test_set_other_name(self): - mocked_set = self.mock_handler.set_displayname - mocked_set.side_effect = SynapseError(400, "message") - - (code, response) = yield self.mock_resource.trigger("PUT", - "/profile/%s/displayname" % ("@opaque:elsewhere"), None) - - self.assertTrue(400 <= code <= 499, - msg="code %d is in the 4xx range" % (code)) - - @defer.inlineCallbacks - def test_get_my_avatar(self): - mocked_get = self.mock_handler.get_avatar_url - mocked_get.return_value = defer.succeed("http://my.server/me.png") - - (code, response) = yield self.mock_resource.trigger("GET", - "/profile/%s/avatar_url" % (myid), None) - - self.assertEquals(200, code) - self.assertEquals({"avatar_url": "http://my.server/me.png"}, response) - self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD") - - @defer.inlineCallbacks - def test_set_my_avatar(self): - mocked_set = self.mock_handler.set_avatar_url - mocked_set.return_value = defer.succeed(()) - - (code, response) = yield self.mock_resource.trigger("PUT", - "/profile/%s/avatar_url" % (myid), - '{"avatar_url": "http://my.server/pic.gif"}') - - self.assertEquals(200, code) - self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD") - self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD") - self.assertEquals(mocked_set.call_args[0][2], - "http://my.server/pic.gif") diff --git a/tests/client/v1/test_rooms.py b/tests/client/v1/test_rooms.py deleted file mode 100644 index 4d529ef007..0000000000 --- a/tests/client/v1/test_rooms.py +++ /dev/null @@ -1,1068 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /rooms paths.""" - -# twisted imports -from twisted.internet import defer - -import synapse.rest.client.v1.room -from synapse.api.constants import Membership - -from synapse.server import HomeServer - -from tests import unittest - -# python imports -import json -import urllib -import types - -from ...utils import MockHttpResource, SQLiteMemoryDbPool, MockKey -from .utils import RestTestCase - -from mock import Mock, NonCallableMock - -PATH_PREFIX = "/_matrix/client/api/v1" - - -class RoomPermissionsTestCase(RestTestCase): - """ Tests room permissions. """ - user_id = "@sid1:red" - rmcreator_id = "@notme:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - self.auth_user_id = self.rmcreator_id - - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - self.auth = hs.get_auth() - - # create some rooms under the name rmcreator_id - self.uncreated_rmid = "!aa:test" - - self.created_rmid = yield self.create_room_as(self.rmcreator_id, - is_public=False) - - 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" % - (self.created_rmid)) - (code, response) = yield self.mock_resource.trigger( - "PUT", - self.created_rmid_msg_path, - '{"msgtype":"m.text","body":"test msg"}') - self.assertEquals(200, code, msg=str(response)) - - # set topic for public room - (code, response) = yield self.mock_resource.trigger( - "PUT", - "/rooms/%s/state/m.room.topic" % self.created_public_rmid, - '{"topic":"Public Room Topic"}') - self.assertEquals(200, code, msg=str(response)) - - # auth as user_id now - self.auth_user_id = self.user_id - - def tearDown(self): - pass - -# @defer.inlineCallbacks -# def test_get_message(self): -# # get message in uncreated room, expect 403 -# (code, response) = yield self.mock_resource.trigger_get( -# "/rooms/noroom/messages/someid/m1") -# self.assertEquals(403, code, msg=str(response)) -# -# # get message in created room not joined (no state), expect 403 -# (code, response) = yield self.mock_resource.trigger_get( -# self.created_rmid_msg_path) -# self.assertEquals(403, code, msg=str(response)) -# -# # get message in created room and invited, expect 403 -# yield self.invite(room=self.created_rmid, src=self.rmcreator_id, -# targ=self.user_id) -# (code, response) = yield self.mock_resource.trigger_get( -# self.created_rmid_msg_path) -# self.assertEquals(403, code, msg=str(response)) -# -# # get message in created room and joined, expect 200 -# yield self.join(room=self.created_rmid, user=self.user_id) -# (code, response) = yield self.mock_resource.trigger_get( -# self.created_rmid_msg_path) -# self.assertEquals(200, code, msg=str(response)) -# -# # get message in created room and left, expect 403 -# yield self.leave(room=self.created_rmid, user=self.user_id) -# (code, response) = yield self.mock_resource.trigger_get( -# self.created_rmid_msg_path) -# self.assertEquals(403, code, msg=str(response)) - - @defer.inlineCallbacks - def test_send_message(self): - msg_content = '{"msgtype":"m.text","body":"hello"}' - send_msg_path = ( - "/rooms/%s/send/m.room.message/mid1" % (self.created_rmid,) - ) - - # send message in uncreated room, expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", - "/rooms/%s/send/m.room.message/mid2" % (self.uncreated_rmid,), - msg_content - ) - self.assertEquals(403, code, msg=str(response)) - - # send message in created room not joined (no state), expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", - send_msg_path, - msg_content - ) - self.assertEquals(403, code, msg=str(response)) - - # send message in created room and invited, expect 403 - yield self.invite( - room=self.created_rmid, - src=self.rmcreator_id, - targ=self.user_id - ) - (code, response) = yield self.mock_resource.trigger( - "PUT", - send_msg_path, - msg_content - ) - self.assertEquals(403, code, msg=str(response)) - - # send message in created room and joined, expect 200 - yield self.join(room=self.created_rmid, user=self.user_id) - (code, response) = yield self.mock_resource.trigger( - "PUT", - send_msg_path, - msg_content - ) - self.assertEquals(200, code, msg=str(response)) - - # send message in created room and left, expect 403 - yield self.leave(room=self.created_rmid, user=self.user_id) - (code, response) = yield self.mock_resource.trigger( - "PUT", - send_msg_path, - msg_content - ) - self.assertEquals(403, code, msg=str(response)) - - @defer.inlineCallbacks - def test_topic_perms(self): - topic_content = '{"topic":"My Topic Name"}' - topic_path = "/rooms/%s/state/m.room.topic" % self.created_rmid - - # set/get topic in uncreated room, expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid, - topic_content) - self.assertEquals(403, code, msg=str(response)) - (code, response) = yield self.mock_resource.trigger_get( - "/rooms/%s/state/m.room.topic" % self.uncreated_rmid) - self.assertEquals(403, code, msg=str(response)) - - # set/get topic in created PRIVATE room not joined, expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", topic_path, topic_content) - self.assertEquals(403, code, msg=str(response)) - (code, response) = yield self.mock_resource.trigger_get(topic_path) - self.assertEquals(403, code, msg=str(response)) - - # set topic in created PRIVATE room and invited, expect 403 - yield self.invite(room=self.created_rmid, src=self.rmcreator_id, - targ=self.user_id) - (code, response) = yield self.mock_resource.trigger( - "PUT", topic_path, topic_content) - self.assertEquals(403, code, msg=str(response)) - - # get topic in created PRIVATE room and invited, expect 403 - (code, response) = yield self.mock_resource.trigger_get(topic_path) - self.assertEquals(403, code, msg=str(response)) - - # set/get topic in created PRIVATE room and joined, expect 200 - yield self.join(room=self.created_rmid, user=self.user_id) - - # Only room ops can set topic by default - self.auth_user_id = self.rmcreator_id - (code, response) = yield self.mock_resource.trigger( - "PUT", topic_path, topic_content) - self.assertEquals(200, code, msg=str(response)) - self.auth_user_id = self.user_id - - (code, response) = yield self.mock_resource.trigger_get(topic_path) - self.assertEquals(200, code, msg=str(response)) - self.assert_dict(json.loads(topic_content), response) - - # set/get topic in created PRIVATE room and left, expect 403 - yield self.leave(room=self.created_rmid, user=self.user_id) - (code, response) = yield self.mock_resource.trigger( - "PUT", topic_path, topic_content) - self.assertEquals(403, code, msg=str(response)) - (code, response) = yield self.mock_resource.trigger_get(topic_path) - self.assertEquals(403, code, msg=str(response)) - - # get topic in PUBLIC room, not joined, expect 403 - (code, response) = yield self.mock_resource.trigger_get( - "/rooms/%s/state/m.room.topic" % self.created_public_rmid) - self.assertEquals(403, code, msg=str(response)) - - # set topic in PUBLIC room, not joined, expect 403 - (code, response) = yield self.mock_resource.trigger( - "PUT", - "/rooms/%s/state/m.room.topic" % self.created_public_rmid, - topic_content) - self.assertEquals(403, code, msg=str(response)) - - @defer.inlineCallbacks - def _test_get_membership(self, room=None, members=[], expect_code=None): - path = "/rooms/%s/state/m.room.member/%s" - for member in members: - (code, response) = yield self.mock_resource.trigger_get( - path % - (room, member)) - self.assertEquals(expect_code, code) - - @defer.inlineCallbacks - def test_membership_basic_room_perms(self): - # === room does not exist === - room = self.uncreated_rmid - # get membership of self, get membership of other, uncreated room - # expect all 403s - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - # trying to invite people to this room should 403 - yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id, - expect_code=403) - - # set [invite/join/left] of self, set [invite/join/left] of other, - # expect all 403s - for usr in [self.user_id, self.rmcreator_id]: - yield self.join(room=room, user=usr, expect_code=404) - yield self.leave(room=room, user=usr, expect_code=403) - - @defer.inlineCallbacks - def test_membership_private_room_perms(self): - room = self.created_rmid - # get membership of self, get membership of other, private room + invite - # expect all 403s - yield self.invite(room=room, src=self.rmcreator_id, - targ=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - # get membership of self, get membership of other, private room + joined - # expect all 200s - yield self.join(room=room, user=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=200) - - # get membership of self, get membership of other, private room + left - # expect all 403s - yield self.leave(room=room, user=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - @defer.inlineCallbacks - def test_membership_public_room_perms(self): - room = self.created_public_rmid - # get membership of self, get membership of other, public room + invite - # expect 403 - yield self.invite(room=room, src=self.rmcreator_id, - targ=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - # get membership of self, get membership of other, public room + joined - # expect all 200s - yield self.join(room=room, user=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=200) - - # get membership of self, get membership of other, public room + left - # expect 403. - yield self.leave(room=room, user=self.user_id) - yield self._test_get_membership( - members=[self.user_id, self.rmcreator_id], - room=room, expect_code=403) - - @defer.inlineCallbacks - def test_invited_permissions(self): - room = self.created_rmid - yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - - # set [invite/join/left] of other user, expect 403s - yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id, - expect_code=403) - yield self.change_membership(room=room, src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.JOIN, - expect_code=403) - yield self.change_membership(room=room, src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.LEAVE, - expect_code=403) - - @defer.inlineCallbacks - def test_joined_permissions(self): - room = self.created_rmid - yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - yield self.join(room=room, user=self.user_id) - - # set invited of self, expect 403 - yield self.invite(room=room, src=self.user_id, targ=self.user_id, - expect_code=403) - - # set joined of self, expect 200 (NOOP) - yield self.join(room=room, user=self.user_id) - - other = "@burgundy:red" - # set invited of other, expect 200 - yield self.invite(room=room, src=self.user_id, targ=other, - expect_code=200) - - # set joined of other, expect 403 - yield self.change_membership(room=room, src=self.user_id, - targ=other, - membership=Membership.JOIN, - expect_code=403) - - # set left of other, expect 403 - yield self.change_membership(room=room, src=self.user_id, - targ=other, - membership=Membership.LEAVE, - expect_code=403) - - # set left of self, expect 200 - yield self.leave(room=room, user=self.user_id) - - @defer.inlineCallbacks - def test_leave_permissions(self): - room = self.created_rmid - yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - yield self.join(room=room, user=self.user_id) - yield self.leave(room=room, user=self.user_id) - - # set [invite/join/left] of self, set [invite/join/left] of other, - # expect all 403s - for usr in [self.user_id, self.rmcreator_id]: - yield self.change_membership( - room=room, - src=self.user_id, - targ=usr, - membership=Membership.INVITE, - expect_code=403 - ) - - yield self.change_membership( - room=room, - src=self.user_id, - targ=usr, - membership=Membership.JOIN, - expect_code=403 - ) - - # It is always valid to LEAVE if you've already left (currently.) - yield self.change_membership( - room=room, - src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.LEAVE, - expect_code=403 - ) - - -class RoomsMemberListTestCase(RestTestCase): - """ Tests /rooms/$room_id/members/list REST events.""" - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - self.auth_user_id = self.user_id - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - def tearDown(self): - pass - - @defer.inlineCallbacks - def test_get_member_list(self): - 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)) - - @defer.inlineCallbacks - def test_get_member_list_no_room(self): - (code, response) = yield self.mock_resource.trigger_get( - "/rooms/roomdoesnotexist/members") - self.assertEquals(403, code, msg=str(response)) - - @defer.inlineCallbacks - def test_get_member_list_no_permission(self): - 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_creator = "@some_other_guy:red" - room_id = yield self.create_room_as(room_creator) - room_path = "/rooms/%s/members" % room_id - yield self.invite(room=room_id, src=room_creator, - targ=self.user_id) - # can't see list if you're just invited. - (code, response) = yield self.mock_resource.trigger_get(room_path) - self.assertEquals(403, code, msg=str(response)) - - yield self.join(room=room_id, user=self.user_id) - # can see list now joined - (code, response) = yield self.mock_resource.trigger_get(room_path) - self.assertEquals(200, code, msg=str(response)) - - yield self.leave(room=room_id, user=self.user_id) - # can no longer see list, you've left. - (code, response) = yield self.mock_resource.trigger_get(room_path) - self.assertEquals(403, code, msg=str(response)) - - -class RoomsCreateTestCase(RestTestCase): - """ Tests /rooms and /rooms/$room_id REST events. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - def tearDown(self): - pass - - @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", - "/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", - "/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", - "/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", - "/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", - "/createRoom", - '{"visibili') - self.assertEquals(400, code) - - (code, response) = yield self.mock_resource.trigger( - "POST", - "/createRoom", - '["hello"]') - self.assertEquals(400, code) - - -class RoomTopicTestCase(RestTestCase): - """ Tests /rooms/$room_id/topic REST events. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - # create the room - 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 - - @defer.inlineCallbacks - def test_invalid_puts(self): - # missing keys or invalid json - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '{}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '{"_name":"bob"}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '{"nao') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '[{"_name":"bob"},{"_name":"jill"}]') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, 'text only') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, '') - self.assertEquals(400, code, msg=str(response)) - - # valid key, wrong type - content = '{"topic":["Topic name"]}' - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, content) - self.assertEquals(400, code, msg=str(response)) - - @defer.inlineCallbacks - def test_rooms_topic(self): - # nothing should be there - (code, response) = yield self.mock_resource.trigger_get(self.path) - self.assertEquals(404, code, msg=str(response)) - - # valid put - content = '{"topic":"Topic name"}' - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, content) - self.assertEquals(200, code, msg=str(response)) - - # valid get - (code, response) = yield self.mock_resource.trigger_get(self.path) - self.assertEquals(200, code, msg=str(response)) - self.assert_dict(json.loads(content), response) - - @defer.inlineCallbacks - def test_rooms_topic_with_extra_keys(self): - # valid put with extra keys - content = '{"topic":"Seasons","subtopic":"Summer"}' - (code, response) = yield self.mock_resource.trigger("PUT", - self.path, content) - self.assertEquals(200, code, msg=str(response)) - - # valid get - (code, response) = yield self.mock_resource.trigger_get(self.path) - self.assertEquals(200, code, msg=str(response)) - self.assert_dict(json.loads(content), response) - - -class RoomMemberStateTestCase(RestTestCase): - """ Tests /rooms/$room_id/members/$user_id/state REST events. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - self.room_id = yield self.create_room_as(self.user_id) - - def tearDown(self): - pass - - @defer.inlineCallbacks - def test_invalid_puts(self): - path = "/rooms/%s/state/m.room.member/%s" % (self.room_id, self.user_id) - # missing keys or invalid json - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{"_name":"bob"}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{"nao') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '[{"_name":"bob"},{"_name":"jill"}]') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, 'text only') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '') - self.assertEquals(400, code, msg=str(response)) - - # valid keys, wrong types - content = ('{"membership":["%s","%s","%s"]}' % - (Membership.INVITE, Membership.JOIN, Membership.LEAVE)) - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(400, code, msg=str(response)) - - @defer.inlineCallbacks - def test_rooms_members_self(self): - path = "/rooms/%s/state/m.room.member/%s" % ( - urllib.quote(self.room_id), self.user_id - ) - - # valid join message (NOOP since we made the room) - content = '{"membership":"%s"}' % Membership.JOIN - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("GET", path, None) - self.assertEquals(200, code, msg=str(response)) - - expected_response = { - "membership": Membership.JOIN, - } - self.assertEquals(expected_response, response) - - @defer.inlineCallbacks - def test_rooms_members_other(self): - self.other_id = "@zzsid1:red" - path = "/rooms/%s/state/m.room.member/%s" % ( - urllib.quote(self.room_id), self.other_id - ) - - # valid invite message - content = '{"membership":"%s"}' % Membership.INVITE - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("GET", path, None) - self.assertEquals(200, code, msg=str(response)) - self.assertEquals(json.loads(content), response) - - @defer.inlineCallbacks - def test_rooms_members_other_custom_keys(self): - self.other_id = "@zzsid1:red" - path = "/rooms/%s/state/m.room.member/%s" % ( - urllib.quote(self.room_id), self.other_id - ) - - # valid invite message with custom key - content = ('{"membership":"%s","invite_text":"%s"}' % - (Membership.INVITE, "Join us!")) - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("GET", path, None) - self.assertEquals(200, code, msg=str(response)) - self.assertEquals(json.loads(content), response) - - -class RoomMessagesTestCase(RestTestCase): - """ Tests /rooms/$room_id/messages/$user_id/$msg_id REST events. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - self.room_id = yield self.create_room_as(self.user_id) - - def tearDown(self): - pass - - @defer.inlineCallbacks - def test_invalid_puts(self): - path = "/rooms/%s/send/m.room.message/mid1" % ( - urllib.quote(self.room_id)) - # missing keys or invalid json - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{"_name":"bob"}') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '{"nao') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '[{"_name":"bob"},{"_name":"jill"}]') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, 'text only') - self.assertEquals(400, code, msg=str(response)) - - (code, response) = yield self.mock_resource.trigger("PUT", - path, '') - self.assertEquals(400, code, msg=str(response)) - - @defer.inlineCallbacks - def test_rooms_messages_sent(self): - path = "/rooms/%s/send/m.room.message/mid1" % ( - urllib.quote(self.room_id)) - - content = '{"body":"test","msgtype":{"type":"a"}}' - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(400, code, msg=str(response)) - - # custom message types - content = '{"body":"test","msgtype":"test.custom.text"}' - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - -# (code, response) = yield self.mock_resource.trigger("GET", path, None) -# self.assertEquals(200, code, msg=str(response)) -# self.assert_dict(json.loads(content), response) - - # m.text message type - path = "/rooms/%s/send/m.room.message/mid2" % ( - urllib.quote(self.room_id)) - content = '{"body":"test2","msgtype":"m.text"}' - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(200, code, msg=str(response)) - - -class RoomInitialSyncTestCase(RestTestCase): - """ Tests /rooms/$room_id/initialSync. """ - user_id = "@sid1:red" - - @defer.inlineCallbacks - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - # Since I'm getting my own presence I need to exist as far as presence - # is concerned. - hs.get_handlers().presence_handler.registered_user( - hs.parse_userid(self.user_id) - ) - - # create the room - self.room_id = yield self.create_room_as(self.user_id) - - @defer.inlineCallbacks - def test_initial_sync(self): - (code, response) = yield self.mock_resource.trigger_get( - "/rooms/%s/initialSync" % self.room_id) - self.assertEquals(200, code) - - self.assertEquals(self.room_id, response["room_id"]) - self.assertEquals("join", response["membership"]) - - # Room state is easier to assert on if we unpack it into a dict - state = {} - for event in response["state"]: - if "state_key" not in event: - continue - t = event["type"] - if t not in state: - state[t] = [] - state[t].append(event) - - self.assertTrue("m.room.create" in state) - - self.assertTrue("messages" in response) - self.assertTrue("chunk" in response["messages"]) - self.assertTrue("end" in response["messages"]) - - self.assertTrue("presence" in response) - - presence_by_user = {e["content"]["user_id"]: e - for e in response["presence"] - } - self.assertTrue(self.user_id in presence_by_user) - self.assertEquals("m.presence", presence_by_user[self.user_id]["type"]) diff --git a/tests/client/v1/test_typing.py b/tests/client/v1/test_typing.py deleted file mode 100644 index af3a9a6c1c..0000000000 --- a/tests/client/v1/test_typing.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /rooms paths.""" - -# twisted imports -from twisted.internet import defer - -import synapse.rest.client.v1.room -from synapse.server import HomeServer - -from ...utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey -from .utils import RestTestCase - -from mock import Mock, NonCallableMock - - -PATH_PREFIX = "/_matrix/client/api/v1" - - -class RoomTypingTestCase(RestTestCase): - """ Tests /rooms/$room_id/typing/$user_id REST API. """ - user_id = "@sid:red" - - @defer.inlineCallbacks - def setUp(self): - self.clock = MockClock() - - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - self.auth_user_id = self.user_id - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - - hs = HomeServer( - "red", - clock=self.clock, - db_pool=db_pool, - http_client=None, - replication_layer=Mock(), - ratelimiter=NonCallableMock(spec_set=[ - "send_message", - ]), - config=self.mock_config, - ) - self.hs = hs - - self.event_source = hs.get_event_sources().sources["typing"] - - self.ratelimiter = hs.get_ratelimiter() - self.ratelimiter.send_message.return_value = (True, 0) - - hs.get_handlers().federation_handler = Mock() - - def _get_user_by_token(token=None): - return { - "user": hs.parse_userid(self.auth_user_id), - "admin": False, - "device_id": None, - } - - hs.get_auth().get_user_by_token = _get_user_by_token - - def _insert_client_ip(*args, **kwargs): - return defer.succeed(None) - hs.get_datastore().insert_client_ip = _insert_client_ip - - def get_room_members(room_id): - if room_id == self.room_id: - return defer.succeed([hs.parse_userid(self.user_id)]) - else: - return defer.succeed([]) - - @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 hs.is_mine(member): - if localusers is not None: - localusers.add(member) - else: - if remotedomains is not None: - remotedomains.add(member.domain) - hs.get_handlers().room_member_handler.fetch_room_distributions_into = ( - fetch_room_distributions_into) - - synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - - self.room_id = yield self.create_room_as(self.user_id) - # Need another user to make notifications actually work - yield self.join(self.room_id, user="@jim:red") - - def tearDown(self): - self.hs.get_handlers().typing_notification_handler.tearDown() - - @defer.inlineCallbacks - def test_set_typing(self): - (code, _) = yield self.mock_resource.trigger("PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - '{"typing": true, "timeout": 30000}' - ) - self.assertEquals(200, code) - - self.assertEquals(self.event_source.get_current_key(), 1) - self.assertEquals( - self.event_source.get_new_events_for_user(self.user_id, 0, None)[0], - [ - {"type": "m.typing", - "room_id": self.room_id, - "content": { - "user_ids": [self.user_id], - }}, - ] - ) - - @defer.inlineCallbacks - def test_set_not_typing(self): - (code, _) = yield self.mock_resource.trigger("PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - '{"typing": false}' - ) - self.assertEquals(200, code) - - @defer.inlineCallbacks - def test_typing_timeout(self): - (code, _) = yield self.mock_resource.trigger("PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - '{"typing": true, "timeout": 30000}' - ) - self.assertEquals(200, code) - - self.assertEquals(self.event_source.get_current_key(), 1) - - self.clock.advance_time(31); - - self.assertEquals(self.event_source.get_current_key(), 2) - - (code, _) = yield self.mock_resource.trigger("PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - '{"typing": true, "timeout": 30000}' - ) - self.assertEquals(200, code) - - self.assertEquals(self.event_source.get_current_key(), 3) diff --git a/tests/client/v1/utils.py b/tests/client/v1/utils.py deleted file mode 100644 index 579441fb4a..0000000000 --- a/tests/client/v1/utils.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 OpenMarket Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# twisted imports -from twisted.internet import defer - -# trial imports -from tests import unittest - -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. - - This subclass assumes there are mock_resource and auth_user_id attributes. - """ - - def __init__(self, *args, **kwargs): - super(RestTestCase, self).__init__(*args, **kwargs) - self.mock_resource = None - self.auth_user_id = None - - def mock_get_user_by_token(self, token=None): - return self.auth_user_id - - @defer.inlineCallbacks - 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 = "/createRoom" - content = "{}" - if not is_public: - content = '{"visibility":"private"}' - if tok: - path = path + "?access_token=%s" % tok - (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): - yield self.change_membership(room=room, src=src, targ=targ, tok=tok, - membership=Membership.INVITE, - expect_code=expect_code) - - @defer.inlineCallbacks - def join(self, room=None, user=None, expect_code=200, tok=None): - yield self.change_membership(room=room, src=user, targ=user, tok=tok, - membership=Membership.JOIN, - expect_code=expect_code) - - @defer.inlineCallbacks - def leave(self, room=None, user=None, expect_code=200, tok=None): - yield self.change_membership(room=room, src=user, targ=user, tok=tok, - membership=Membership.LEAVE, - expect_code=expect_code) - - @defer.inlineCallbacks - def change_membership(self, room, src, targ, membership, tok=None, - expect_code=200): - temp_id = self.auth_user_id - self.auth_user_id = src - - path = "/rooms/%s/state/m.room.member/%s" % (room, targ) - if tok: - path = path + "?access_token=%s" % tok - - data = { - "membership": membership - } - - (code, response) = yield self.mock_resource.trigger("PUT", path, - json.dumps(data)) - self.assertEquals(expect_code, code, msg=str(response)) - - self.auth_user_id = temp_id - - @defer.inlineCallbacks - def register(self, user_id): - (code, response) = yield self.mock_resource.trigger( - "POST", - "/register", - json.dumps({ - "user": user_id, - "password": "test", - "type": "m.login.password" - })) - self.assertEquals(200, code) - defer.returnValue(response) - - @defer.inlineCallbacks - def send(self, room_id, body=None, txn_id=None, tok=None, - expect_code=200): - if txn_id is None: - txn_id = "m%s" % (str(time.time())) - if body is None: - body = "body_text_here" - - path = "/rooms/%s/send/m.room.message/%s" % (room_id, txn_id) - content = '{"msgtype":"m.text","body":"%s"}' % body - if tok: - path = path + "?access_token=%s" % tok - - (code, response) = yield self.mock_resource.trigger("PUT", path, content) - self.assertEquals(expect_code, code, msg=str(response)) - - def assert_dict(self, required, actual): - """Does a partial assert of a dict. - - Args: - required (dict): The keys and value which MUST be in 'actual'. - actual (dict): The test result. Extra keys will not be checked. - """ - for key in required: - self.assertEquals(required[key], actual[key], - msg="%s mismatch. %s" % (key, actual)) diff --git a/tests/rest/__init__.py b/tests/rest/__init__.py new file mode 100644 index 0000000000..1a84d94cd9 --- /dev/null +++ b/tests/rest/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/rest/client/__init__.py b/tests/rest/client/__init__.py new file mode 100644 index 0000000000..1a84d94cd9 --- /dev/null +++ b/tests/rest/client/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/rest/client/v1/__init__.py b/tests/rest/client/v1/__init__.py new file mode 100644 index 0000000000..9bff9ec169 --- /dev/null +++ b/tests/rest/client/v1/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py new file mode 100644 index 0000000000..0384ffbb3d --- /dev/null +++ b/tests/rest/client/v1/test_events.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Tests REST events for /events paths.""" +from tests import unittest + +# twisted imports +from twisted.internet import defer + +import synapse.rest.client.v1.events +import synapse.rest.client.v1.register +import synapse.rest.client.v1.room + +from synapse.server import HomeServer + +from ....utils import MockHttpResource, SQLiteMemoryDbPool, MockKey +from .utils import RestTestCase + +from mock import Mock, NonCallableMock + + +PATH_PREFIX = "/_matrix/client/api/v1" + + +class EventStreamPaginationApiTestCase(unittest.TestCase): + """ Tests event streaming query parameters and start/end keys used in the + Pagination stream API. """ + user_id = "sid1" + + def setUp(self): + # configure stream and inject items + pass + + def tearDown(self): + pass + + def TODO_test_long_poll(self): + # stream from 'end' key, send (self+other) message, expect message. + + # stream from 'END', send (self+other) message, expect message. + + # stream from 'end' key, send (self+other) topic, expect topic. + + # stream from 'END', send (self+other) topic, expect topic. + + # stream from 'end' key, send (self+other) invite, expect invite. + + # stream from 'END', send (self+other) invite, expect invite. + + pass + + def TODO_test_stream_forward(self): + # stream from START, expect injected items + + # stream from 'start' key, expect same content + + # stream from 'end' key, expect nothing + + # stream from 'END', expect nothing + + # The following is needed for cases where content is removed e.g. you + # left a room, so the token you're streaming from is > the one that + # would be returned naturally from START>END. + # stream from very new token (higher than end key), expect same token + # returned as end key + pass + + def TODO_test_limits(self): + # stream from a key, expect limit_num items + + # stream from START, expect limit_num items + + pass + + def TODO_test_range(self): + # stream from key to key, expect X items + + # stream from key to END, expect X items + + # stream from START to key, expect X items + + # stream from START to END, expect all items + pass + + def TODO_test_direction(self): + # stream from END to START and fwds, expect newest first + + # stream from END to START and bwds, expect oldest first + + # stream from START to END and fwds, expect oldest first + + # stream from START to END and bwds, expect newest first + + pass + + +class EventStreamPermissionsTestCase(RestTestCase): + """ Tests event streaming (GET /events). """ + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "test", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + clock=Mock(spec=[ + "call_later", + "cancel_call_later", + "time_msec", + "time" + ]), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + hs.config.enable_registration_captcha = False + + hs.get_handlers().federation_handler = Mock() + + hs.get_clock().time_msec.return_value = 1000000 + hs.get_clock().time.return_value = 1000 + + synapse.rest.client.v1.register.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.events.register_servlets(hs, self.mock_resource) + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + # register an account + self.user_id = "sid1" + response = yield self.register(self.user_id) + self.token = response["access_token"] + self.user_id = response["user_id"] + + # register a 2nd account + self.other_user = "other1" + response = yield self.register(self.other_user) + self.other_token = response["access_token"] + self.other_user = response["user_id"] + + def tearDown(self): + pass + + @defer.inlineCallbacks + def test_stream_basic_permissions(self): + # invalid token, expect 403 + (code, response) = yield self.mock_resource.trigger_get( + "/events?access_token=%s" % ("invalid" + self.token, ) + ) + self.assertEquals(403, code, msg=str(response)) + + # valid token, expect content + (code, response) = yield self.mock_resource.trigger_get( + "/events?access_token=%s&timeout=0" % (self.token,) + ) + self.assertEquals(200, code, msg=str(response)) + self.assertTrue("chunk" in response) + self.assertTrue("start" in response) + self.assertTrue("end" in response) + + @defer.inlineCallbacks + def test_stream_room_permissions(self): + 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) + yield self.invite( + room_id, + src=self.other_user, + targ=self.user_id, + tok=self.other_token + ) + + (code, response) = yield self.mock_resource.trigger_get( + "/events?access_token=%s&timeout=0" % (self.token,) + ) + self.assertEquals(200, code, msg=str(response)) + + self.assertEquals(0, len(response["chunk"])) + + # joined room (expect all content for room) + yield self.join(room=room_id, user=self.user_id, tok=self.token) + + # left to room (expect no content for room) + + def TODO_test_stream_items(self): + # new user, no content + + # join room, expect 1 item (join) + + # send message, expect 2 items (join,send) + + # set topic, expect 3 items (join,send,topic) + + # someone else join room, expect 4 (join,send,topic,join) + + # someone else send message, expect 5 (join,send.topic,join,send) + + # someone else set topic, expect 6 (join,send,topic,join,send,topic) + pass diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py new file mode 100644 index 0000000000..0b6f7cfccb --- /dev/null +++ b/tests/rest/client/v1/test_presence.py @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests REST events for /presence paths.""" + +from tests import unittest +from twisted.internet import defer + +from mock import Mock + +from ....utils import MockHttpResource, MockKey + +from synapse.api.constants import PresenceState +from synapse.handlers.presence import PresenceHandler +from synapse.server import HomeServer + + +OFFLINE = PresenceState.OFFLINE +UNAVAILABLE = PresenceState.UNAVAILABLE +ONLINE = PresenceState.ONLINE + + +myid = "@apple:test" +PATH_PREFIX = "/_matrix/client/api/v1" + + +class JustPresenceHandlers(object): + def __init__(self, hs): + self.presence_handler = PresenceHandler(hs) + + +class PresenceStateTestCase(unittest.TestCase): + + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.mock_config = Mock() + self.mock_config.signing_key = [MockKey()] + hs = HomeServer("test", + db_pool=None, + datastore=Mock(spec=[ + "get_presence_state", + "set_presence_state", + "insert_client_ip", + ]), + http_client=None, + resource_for_client=self.mock_resource, + resource_for_federation=self.mock_resource, + config=self.mock_config, + ) + hs.handlers = JustPresenceHandlers(hs) + + self.datastore = hs.get_datastore() + + def get_presence_list(*a, **kw): + return defer.succeed([]) + self.datastore.get_presence_list = get_presence_list + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(myid), + "admin": False, + "device_id": None, + } + + hs.get_auth().get_user_by_token = _get_user_by_token + + room_member_handler = hs.handlers.room_member_handler = Mock( + spec=[ + "get_rooms_for_user", + ] + ) + + def get_rooms_for_user(user): + return defer.succeed([]) + room_member_handler.get_rooms_for_user = get_rooms_for_user + + hs.register_servlets() + + self.u_apple = hs.parse_userid(myid) + + @defer.inlineCallbacks + def test_get_my_status(self): + mocked_get = self.datastore.get_presence_state + mocked_get.return_value = defer.succeed( + {"state": ONLINE, "status_msg": "Available"} + ) + + (code, response) = yield self.mock_resource.trigger("GET", + "/presence/%s/status" % (myid), None) + + self.assertEquals(200, code) + self.assertEquals( + {"presence": ONLINE, "status_msg": "Available"}, + response + ) + mocked_get.assert_called_with("apple") + + @defer.inlineCallbacks + def test_set_my_status(self): + mocked_set = self.datastore.set_presence_state + mocked_set.return_value = defer.succeed({"state": OFFLINE}) + + (code, response) = yield self.mock_resource.trigger("PUT", + "/presence/%s/status" % (myid), + '{"presence": "unavailable", "status_msg": "Away"}') + + self.assertEquals(200, code) + mocked_set.assert_called_with("apple", + {"state": UNAVAILABLE, "status_msg": "Away"} + ) + + +class PresenceListTestCase(unittest.TestCase): + + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.mock_config = Mock() + self.mock_config.signing_key = [MockKey()] + + hs = HomeServer("test", + db_pool=None, + datastore=Mock(spec=[ + "has_presence_state", + "get_presence_state", + "allow_presence_visible", + "is_presence_visible", + "add_presence_list_pending", + "set_presence_list_accepted", + "del_presence_list", + "get_presence_list", + "insert_client_ip", + ]), + http_client=None, + resource_for_client=self.mock_resource, + resource_for_federation=self.mock_resource, + config=self.mock_config, + ) + hs.handlers = JustPresenceHandlers(hs) + + self.datastore = hs.get_datastore() + + def has_presence_state(user_localpart): + return defer.succeed( + user_localpart in ("apple", "banana",) + ) + self.datastore.has_presence_state = has_presence_state + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(myid), + "admin": False, + "device_id": None, + } + + room_member_handler = hs.handlers.room_member_handler = Mock( + spec=[ + "get_rooms_for_user", + ] + ) + + hs.get_auth().get_user_by_token = _get_user_by_token + + hs.register_servlets() + + self.u_apple = hs.parse_userid("@apple:test") + self.u_banana = hs.parse_userid("@banana:test") + + @defer.inlineCallbacks + def test_get_my_list(self): + self.datastore.get_presence_list.return_value = defer.succeed( + [{"observed_user_id": "@banana:test"}], + ) + + (code, response) = yield self.mock_resource.trigger("GET", + "/presence/list/%s" % (myid), None) + + self.assertEquals(200, code) + self.assertEquals([ + {"user_id": "@banana:test", "presence": OFFLINE}, + ], response) + + self.datastore.get_presence_list.assert_called_with( + "apple", accepted=True + ) + + @defer.inlineCallbacks + def test_invite(self): + self.datastore.add_presence_list_pending.return_value = ( + defer.succeed(()) + ) + self.datastore.is_presence_visible.return_value = defer.succeed( + True + ) + + (code, response) = yield self.mock_resource.trigger("POST", + "/presence/list/%s" % (myid), + """{"invite": ["@banana:test"]}""" + ) + + self.assertEquals(200, code) + + self.datastore.add_presence_list_pending.assert_called_with( + "apple", "@banana:test" + ) + self.datastore.set_presence_list_accepted.assert_called_with( + "apple", "@banana:test" + ) + + @defer.inlineCallbacks + def test_drop(self): + self.datastore.del_presence_list.return_value = ( + defer.succeed(()) + ) + + (code, response) = yield self.mock_resource.trigger("POST", + "/presence/list/%s" % (myid), + """{"drop": ["@banana:test"]}""" + ) + + self.assertEquals(200, code) + + self.datastore.del_presence_list.assert_called_with( + "apple", "@banana:test" + ) + + +class PresenceEventStreamTestCase(unittest.TestCase): + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + self.mock_config = Mock() + self.mock_config.signing_key = [MockKey()] + + # HIDEOUS HACKERY + # TODO(paul): This should be injected in via the HomeServer DI system + from synapse.streams.events import ( + PresenceEventSource, NullSource, EventSources + ) + + old_SOURCE_TYPES = EventSources.SOURCE_TYPES + def tearDown(): + EventSources.SOURCE_TYPES = old_SOURCE_TYPES + self.tearDown = tearDown + + EventSources.SOURCE_TYPES = { + k: NullSource for k in old_SOURCE_TYPES.keys() + } + EventSources.SOURCE_TYPES["presence"] = PresenceEventSource + + hs = HomeServer("test", + db_pool=None, + http_client=None, + resource_for_client=self.mock_resource, + resource_for_federation=self.mock_resource, + datastore=Mock(spec=[ + "set_presence_state", + "get_presence_list", + ]), + clock=Mock(spec=[ + "call_later", + "cancel_call_later", + "time_msec", + ]), + config=self.mock_config, + ) + + hs.get_clock().time_msec.return_value = 1000000 + + def _get_user_by_req(req=None): + return hs.parse_userid(myid) + + hs.get_auth().get_user_by_req = _get_user_by_req + + hs.register_servlets() + + hs.handlers.room_member_handler = Mock(spec=[]) + + self.room_members = [] + + def get_rooms_for_user(user): + if user in self.room_members: + return ["a-room"] + else: + return [] + hs.handlers.room_member_handler.get_rooms_for_user = get_rooms_for_user + + self.mock_datastore = hs.get_datastore() + + def get_profile_displayname(user_id): + return defer.succeed("Frank") + self.mock_datastore.get_profile_displayname = get_profile_displayname + + def get_profile_avatar_url(user_id): + return defer.succeed(None) + self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url + + def user_rooms_intersect(user_list): + room_member_ids = map(lambda u: u.to_string(), self.room_members) + + shared = all(map(lambda i: i in room_member_ids, user_list)) + return defer.succeed(shared) + self.mock_datastore.user_rooms_intersect = user_rooms_intersect + + def get_joined_hosts_for_room(room_id): + return [] + self.mock_datastore.get_joined_hosts_for_room = get_joined_hosts_for_room + + self.presence = hs.get_handlers().presence_handler + + self.u_apple = hs.parse_userid("@apple:test") + self.u_banana = hs.parse_userid("@banana:test") + + @defer.inlineCallbacks + def test_shortpoll(self): + self.room_members = [self.u_apple, self.u_banana] + + self.mock_datastore.set_presence_state.return_value = defer.succeed( + {"state": ONLINE} + ) + self.mock_datastore.get_presence_list.return_value = defer.succeed( + [] + ) + + (code, response) = yield self.mock_resource.trigger("GET", + "/events?timeout=0", None) + + self.assertEquals(200, code) + + # We've forced there to be only one data stream so the tokens will + # all be ours + + # I'll already get my own presence state change + self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []}, + response + ) + + self.mock_datastore.set_presence_state.return_value = defer.succeed( + {"state": ONLINE} + ) + self.mock_datastore.get_presence_list.return_value = defer.succeed( + [] + ) + + yield self.presence.set_state(self.u_banana, self.u_banana, + state={"presence": ONLINE} + ) + + (code, response) = yield self.mock_resource.trigger("GET", + "/events?from=0_1_0&timeout=0", None) + + self.assertEquals(200, code) + self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [ + {"type": "m.presence", + "content": { + "user_id": "@banana:test", + "presence": ONLINE, + "displayname": "Frank", + "last_active_ago": 0, + }}, + ]}, response) diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py new file mode 100644 index 0000000000..47cfb10a6d --- /dev/null +++ b/tests/rest/client/v1/test_profile.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests REST events for /profile paths.""" + +from tests import unittest +from twisted.internet import defer + +from mock import Mock, NonCallableMock + +from ....utils import MockHttpResource, MockKey + +from synapse.api.errors import SynapseError, AuthError +from synapse.server import HomeServer + +myid = "@1234ABCD:test" +PATH_PREFIX = "/_matrix/client/api/v1" + + +class ProfileTestCase(unittest.TestCase): + """ Tests profile management. """ + + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.mock_handler = Mock(spec=[ + "get_displayname", + "set_displayname", + "get_avatar_url", + "set_avatar_url", + ]) + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + hs = HomeServer("test", + db_pool=None, + http_client=None, + resource_for_client=self.mock_resource, + federation=Mock(), + replication_layer=Mock(), + datastore=None, + config=self.mock_config, + ) + + def _get_user_by_req(request=None): + return hs.parse_userid(myid) + + hs.get_auth().get_user_by_req = _get_user_by_req + + hs.get_handlers().profile_handler = self.mock_handler + + hs.register_servlets() + + @defer.inlineCallbacks + def test_get_my_name(self): + mocked_get = self.mock_handler.get_displayname + mocked_get.return_value = defer.succeed("Frank") + + (code, response) = yield self.mock_resource.trigger("GET", + "/profile/%s/displayname" % (myid), None) + + self.assertEquals(200, code) + self.assertEquals({"displayname": "Frank"}, response) + self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD") + + @defer.inlineCallbacks + def test_set_my_name(self): + mocked_set = self.mock_handler.set_displayname + mocked_set.return_value = defer.succeed(()) + + (code, response) = yield self.mock_resource.trigger("PUT", + "/profile/%s/displayname" % (myid), + '{"displayname": "Frank Jr."}') + + self.assertEquals(200, code) + self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD") + self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD") + self.assertEquals(mocked_set.call_args[0][2], "Frank Jr.") + + @defer.inlineCallbacks + def test_set_my_name_noauth(self): + mocked_set = self.mock_handler.set_displayname + mocked_set.side_effect = AuthError(400, "message") + + (code, response) = yield self.mock_resource.trigger("PUT", + "/profile/%s/displayname" % ("@4567:test"), '"Frank Jr."') + + self.assertTrue(400 <= code < 499, + msg="code %d is in the 4xx range" % (code)) + + @defer.inlineCallbacks + def test_get_other_name(self): + mocked_get = self.mock_handler.get_displayname + mocked_get.return_value = defer.succeed("Bob") + + (code, response) = yield self.mock_resource.trigger("GET", + "/profile/%s/displayname" % ("@opaque:elsewhere"), None) + + self.assertEquals(200, code) + self.assertEquals({"displayname": "Bob"}, response) + + @defer.inlineCallbacks + def test_set_other_name(self): + mocked_set = self.mock_handler.set_displayname + mocked_set.side_effect = SynapseError(400, "message") + + (code, response) = yield self.mock_resource.trigger("PUT", + "/profile/%s/displayname" % ("@opaque:elsewhere"), None) + + self.assertTrue(400 <= code <= 499, + msg="code %d is in the 4xx range" % (code)) + + @defer.inlineCallbacks + def test_get_my_avatar(self): + mocked_get = self.mock_handler.get_avatar_url + mocked_get.return_value = defer.succeed("http://my.server/me.png") + + (code, response) = yield self.mock_resource.trigger("GET", + "/profile/%s/avatar_url" % (myid), None) + + self.assertEquals(200, code) + self.assertEquals({"avatar_url": "http://my.server/me.png"}, response) + self.assertEquals(mocked_get.call_args[0][0].localpart, "1234ABCD") + + @defer.inlineCallbacks + def test_set_my_avatar(self): + mocked_set = self.mock_handler.set_avatar_url + mocked_set.return_value = defer.succeed(()) + + (code, response) = yield self.mock_resource.trigger("PUT", + "/profile/%s/avatar_url" % (myid), + '{"avatar_url": "http://my.server/pic.gif"}') + + self.assertEquals(200, code) + self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD") + self.assertEquals(mocked_set.call_args[0][1].localpart, "1234ABCD") + self.assertEquals(mocked_set.call_args[0][2], + "http://my.server/pic.gif") diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py new file mode 100644 index 0000000000..12f8040541 --- /dev/null +++ b/tests/rest/client/v1/test_rooms.py @@ -0,0 +1,1068 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests REST events for /rooms paths.""" + +# twisted imports +from twisted.internet import defer + +import synapse.rest.client.v1.room +from synapse.api.constants import Membership + +from synapse.server import HomeServer + +from tests import unittest + +# python imports +import json +import urllib +import types + +from ....utils import MockHttpResource, SQLiteMemoryDbPool, MockKey +from .utils import RestTestCase + +from mock import Mock, NonCallableMock + +PATH_PREFIX = "/_matrix/client/api/v1" + + +class RoomPermissionsTestCase(RestTestCase): + """ Tests room permissions. """ + user_id = "@sid1:red" + rmcreator_id = "@notme:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + self.auth_user_id = self.rmcreator_id + + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + self.auth = hs.get_auth() + + # create some rooms under the name rmcreator_id + self.uncreated_rmid = "!aa:test" + + self.created_rmid = yield self.create_room_as(self.rmcreator_id, + is_public=False) + + 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" % + (self.created_rmid)) + (code, response) = yield self.mock_resource.trigger( + "PUT", + self.created_rmid_msg_path, + '{"msgtype":"m.text","body":"test msg"}') + self.assertEquals(200, code, msg=str(response)) + + # set topic for public room + (code, response) = yield self.mock_resource.trigger( + "PUT", + "/rooms/%s/state/m.room.topic" % self.created_public_rmid, + '{"topic":"Public Room Topic"}') + self.assertEquals(200, code, msg=str(response)) + + # auth as user_id now + self.auth_user_id = self.user_id + + def tearDown(self): + pass + +# @defer.inlineCallbacks +# def test_get_message(self): +# # get message in uncreated room, expect 403 +# (code, response) = yield self.mock_resource.trigger_get( +# "/rooms/noroom/messages/someid/m1") +# self.assertEquals(403, code, msg=str(response)) +# +# # get message in created room not joined (no state), expect 403 +# (code, response) = yield self.mock_resource.trigger_get( +# self.created_rmid_msg_path) +# self.assertEquals(403, code, msg=str(response)) +# +# # get message in created room and invited, expect 403 +# yield self.invite(room=self.created_rmid, src=self.rmcreator_id, +# targ=self.user_id) +# (code, response) = yield self.mock_resource.trigger_get( +# self.created_rmid_msg_path) +# self.assertEquals(403, code, msg=str(response)) +# +# # get message in created room and joined, expect 200 +# yield self.join(room=self.created_rmid, user=self.user_id) +# (code, response) = yield self.mock_resource.trigger_get( +# self.created_rmid_msg_path) +# self.assertEquals(200, code, msg=str(response)) +# +# # get message in created room and left, expect 403 +# yield self.leave(room=self.created_rmid, user=self.user_id) +# (code, response) = yield self.mock_resource.trigger_get( +# self.created_rmid_msg_path) +# self.assertEquals(403, code, msg=str(response)) + + @defer.inlineCallbacks + def test_send_message(self): + msg_content = '{"msgtype":"m.text","body":"hello"}' + send_msg_path = ( + "/rooms/%s/send/m.room.message/mid1" % (self.created_rmid,) + ) + + # send message in uncreated room, expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", + "/rooms/%s/send/m.room.message/mid2" % (self.uncreated_rmid,), + msg_content + ) + self.assertEquals(403, code, msg=str(response)) + + # send message in created room not joined (no state), expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", + send_msg_path, + msg_content + ) + self.assertEquals(403, code, msg=str(response)) + + # send message in created room and invited, expect 403 + yield self.invite( + room=self.created_rmid, + src=self.rmcreator_id, + targ=self.user_id + ) + (code, response) = yield self.mock_resource.trigger( + "PUT", + send_msg_path, + msg_content + ) + self.assertEquals(403, code, msg=str(response)) + + # send message in created room and joined, expect 200 + yield self.join(room=self.created_rmid, user=self.user_id) + (code, response) = yield self.mock_resource.trigger( + "PUT", + send_msg_path, + msg_content + ) + self.assertEquals(200, code, msg=str(response)) + + # send message in created room and left, expect 403 + yield self.leave(room=self.created_rmid, user=self.user_id) + (code, response) = yield self.mock_resource.trigger( + "PUT", + send_msg_path, + msg_content + ) + self.assertEquals(403, code, msg=str(response)) + + @defer.inlineCallbacks + def test_topic_perms(self): + topic_content = '{"topic":"My Topic Name"}' + topic_path = "/rooms/%s/state/m.room.topic" % self.created_rmid + + # set/get topic in uncreated room, expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid, + topic_content) + self.assertEquals(403, code, msg=str(response)) + (code, response) = yield self.mock_resource.trigger_get( + "/rooms/%s/state/m.room.topic" % self.uncreated_rmid) + self.assertEquals(403, code, msg=str(response)) + + # set/get topic in created PRIVATE room not joined, expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", topic_path, topic_content) + self.assertEquals(403, code, msg=str(response)) + (code, response) = yield self.mock_resource.trigger_get(topic_path) + self.assertEquals(403, code, msg=str(response)) + + # set topic in created PRIVATE room and invited, expect 403 + yield self.invite(room=self.created_rmid, src=self.rmcreator_id, + targ=self.user_id) + (code, response) = yield self.mock_resource.trigger( + "PUT", topic_path, topic_content) + self.assertEquals(403, code, msg=str(response)) + + # get topic in created PRIVATE room and invited, expect 403 + (code, response) = yield self.mock_resource.trigger_get(topic_path) + self.assertEquals(403, code, msg=str(response)) + + # set/get topic in created PRIVATE room and joined, expect 200 + yield self.join(room=self.created_rmid, user=self.user_id) + + # Only room ops can set topic by default + self.auth_user_id = self.rmcreator_id + (code, response) = yield self.mock_resource.trigger( + "PUT", topic_path, topic_content) + self.assertEquals(200, code, msg=str(response)) + self.auth_user_id = self.user_id + + (code, response) = yield self.mock_resource.trigger_get(topic_path) + self.assertEquals(200, code, msg=str(response)) + self.assert_dict(json.loads(topic_content), response) + + # set/get topic in created PRIVATE room and left, expect 403 + yield self.leave(room=self.created_rmid, user=self.user_id) + (code, response) = yield self.mock_resource.trigger( + "PUT", topic_path, topic_content) + self.assertEquals(403, code, msg=str(response)) + (code, response) = yield self.mock_resource.trigger_get(topic_path) + self.assertEquals(403, code, msg=str(response)) + + # get topic in PUBLIC room, not joined, expect 403 + (code, response) = yield self.mock_resource.trigger_get( + "/rooms/%s/state/m.room.topic" % self.created_public_rmid) + self.assertEquals(403, code, msg=str(response)) + + # set topic in PUBLIC room, not joined, expect 403 + (code, response) = yield self.mock_resource.trigger( + "PUT", + "/rooms/%s/state/m.room.topic" % self.created_public_rmid, + topic_content) + self.assertEquals(403, code, msg=str(response)) + + @defer.inlineCallbacks + def _test_get_membership(self, room=None, members=[], expect_code=None): + path = "/rooms/%s/state/m.room.member/%s" + for member in members: + (code, response) = yield self.mock_resource.trigger_get( + path % + (room, member)) + self.assertEquals(expect_code, code) + + @defer.inlineCallbacks + def test_membership_basic_room_perms(self): + # === room does not exist === + room = self.uncreated_rmid + # get membership of self, get membership of other, uncreated room + # expect all 403s + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + # trying to invite people to this room should 403 + yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id, + expect_code=403) + + # set [invite/join/left] of self, set [invite/join/left] of other, + # expect all 403s + for usr in [self.user_id, self.rmcreator_id]: + yield self.join(room=room, user=usr, expect_code=404) + yield self.leave(room=room, user=usr, expect_code=403) + + @defer.inlineCallbacks + def test_membership_private_room_perms(self): + room = self.created_rmid + # get membership of self, get membership of other, private room + invite + # expect all 403s + yield self.invite(room=room, src=self.rmcreator_id, + targ=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + # get membership of self, get membership of other, private room + joined + # expect all 200s + yield self.join(room=room, user=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=200) + + # get membership of self, get membership of other, private room + left + # expect all 403s + yield self.leave(room=room, user=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + @defer.inlineCallbacks + def test_membership_public_room_perms(self): + room = self.created_public_rmid + # get membership of self, get membership of other, public room + invite + # expect 403 + yield self.invite(room=room, src=self.rmcreator_id, + targ=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + # get membership of self, get membership of other, public room + joined + # expect all 200s + yield self.join(room=room, user=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=200) + + # get membership of self, get membership of other, public room + left + # expect 403. + yield self.leave(room=room, user=self.user_id) + yield self._test_get_membership( + members=[self.user_id, self.rmcreator_id], + room=room, expect_code=403) + + @defer.inlineCallbacks + def test_invited_permissions(self): + room = self.created_rmid + yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) + + # set [invite/join/left] of other user, expect 403s + yield self.invite(room=room, src=self.user_id, targ=self.rmcreator_id, + expect_code=403) + yield self.change_membership(room=room, src=self.user_id, + targ=self.rmcreator_id, + membership=Membership.JOIN, + expect_code=403) + yield self.change_membership(room=room, src=self.user_id, + targ=self.rmcreator_id, + membership=Membership.LEAVE, + expect_code=403) + + @defer.inlineCallbacks + def test_joined_permissions(self): + room = self.created_rmid + yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) + yield self.join(room=room, user=self.user_id) + + # set invited of self, expect 403 + yield self.invite(room=room, src=self.user_id, targ=self.user_id, + expect_code=403) + + # set joined of self, expect 200 (NOOP) + yield self.join(room=room, user=self.user_id) + + other = "@burgundy:red" + # set invited of other, expect 200 + yield self.invite(room=room, src=self.user_id, targ=other, + expect_code=200) + + # set joined of other, expect 403 + yield self.change_membership(room=room, src=self.user_id, + targ=other, + membership=Membership.JOIN, + expect_code=403) + + # set left of other, expect 403 + yield self.change_membership(room=room, src=self.user_id, + targ=other, + membership=Membership.LEAVE, + expect_code=403) + + # set left of self, expect 200 + yield self.leave(room=room, user=self.user_id) + + @defer.inlineCallbacks + def test_leave_permissions(self): + room = self.created_rmid + yield self.invite(room=room, src=self.rmcreator_id, targ=self.user_id) + yield self.join(room=room, user=self.user_id) + yield self.leave(room=room, user=self.user_id) + + # set [invite/join/left] of self, set [invite/join/left] of other, + # expect all 403s + for usr in [self.user_id, self.rmcreator_id]: + yield self.change_membership( + room=room, + src=self.user_id, + targ=usr, + membership=Membership.INVITE, + expect_code=403 + ) + + yield self.change_membership( + room=room, + src=self.user_id, + targ=usr, + membership=Membership.JOIN, + expect_code=403 + ) + + # It is always valid to LEAVE if you've already left (currently.) + yield self.change_membership( + room=room, + src=self.user_id, + targ=self.rmcreator_id, + membership=Membership.LEAVE, + expect_code=403 + ) + + +class RoomsMemberListTestCase(RestTestCase): + """ Tests /rooms/$room_id/members/list REST events.""" + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + self.auth_user_id = self.user_id + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + def tearDown(self): + pass + + @defer.inlineCallbacks + def test_get_member_list(self): + 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)) + + @defer.inlineCallbacks + def test_get_member_list_no_room(self): + (code, response) = yield self.mock_resource.trigger_get( + "/rooms/roomdoesnotexist/members") + self.assertEquals(403, code, msg=str(response)) + + @defer.inlineCallbacks + def test_get_member_list_no_permission(self): + 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_creator = "@some_other_guy:red" + room_id = yield self.create_room_as(room_creator) + room_path = "/rooms/%s/members" % room_id + yield self.invite(room=room_id, src=room_creator, + targ=self.user_id) + # can't see list if you're just invited. + (code, response) = yield self.mock_resource.trigger_get(room_path) + self.assertEquals(403, code, msg=str(response)) + + yield self.join(room=room_id, user=self.user_id) + # can see list now joined + (code, response) = yield self.mock_resource.trigger_get(room_path) + self.assertEquals(200, code, msg=str(response)) + + yield self.leave(room=room_id, user=self.user_id) + # can no longer see list, you've left. + (code, response) = yield self.mock_resource.trigger_get(room_path) + self.assertEquals(403, code, msg=str(response)) + + +class RoomsCreateTestCase(RestTestCase): + """ Tests /rooms and /rooms/$room_id REST events. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + def tearDown(self): + pass + + @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", + "/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", + "/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", + "/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", + "/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", + "/createRoom", + '{"visibili') + self.assertEquals(400, code) + + (code, response) = yield self.mock_resource.trigger( + "POST", + "/createRoom", + '["hello"]') + self.assertEquals(400, code) + + +class RoomTopicTestCase(RestTestCase): + """ Tests /rooms/$room_id/topic REST events. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + # create the room + 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 + + @defer.inlineCallbacks + def test_invalid_puts(self): + # missing keys or invalid json + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '{}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '{"_name":"bob"}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '{"nao') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '[{"_name":"bob"},{"_name":"jill"}]') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, 'text only') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, '') + self.assertEquals(400, code, msg=str(response)) + + # valid key, wrong type + content = '{"topic":["Topic name"]}' + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, content) + self.assertEquals(400, code, msg=str(response)) + + @defer.inlineCallbacks + def test_rooms_topic(self): + # nothing should be there + (code, response) = yield self.mock_resource.trigger_get(self.path) + self.assertEquals(404, code, msg=str(response)) + + # valid put + content = '{"topic":"Topic name"}' + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, content) + self.assertEquals(200, code, msg=str(response)) + + # valid get + (code, response) = yield self.mock_resource.trigger_get(self.path) + self.assertEquals(200, code, msg=str(response)) + self.assert_dict(json.loads(content), response) + + @defer.inlineCallbacks + def test_rooms_topic_with_extra_keys(self): + # valid put with extra keys + content = '{"topic":"Seasons","subtopic":"Summer"}' + (code, response) = yield self.mock_resource.trigger("PUT", + self.path, content) + self.assertEquals(200, code, msg=str(response)) + + # valid get + (code, response) = yield self.mock_resource.trigger_get(self.path) + self.assertEquals(200, code, msg=str(response)) + self.assert_dict(json.loads(content), response) + + +class RoomMemberStateTestCase(RestTestCase): + """ Tests /rooms/$room_id/members/$user_id/state REST events. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + self.room_id = yield self.create_room_as(self.user_id) + + def tearDown(self): + pass + + @defer.inlineCallbacks + def test_invalid_puts(self): + path = "/rooms/%s/state/m.room.member/%s" % (self.room_id, self.user_id) + # missing keys or invalid json + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{"_name":"bob"}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{"nao') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '[{"_name":"bob"},{"_name":"jill"}]') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, 'text only') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '') + self.assertEquals(400, code, msg=str(response)) + + # valid keys, wrong types + content = ('{"membership":["%s","%s","%s"]}' % + (Membership.INVITE, Membership.JOIN, Membership.LEAVE)) + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(400, code, msg=str(response)) + + @defer.inlineCallbacks + def test_rooms_members_self(self): + path = "/rooms/%s/state/m.room.member/%s" % ( + urllib.quote(self.room_id), self.user_id + ) + + # valid join message (NOOP since we made the room) + content = '{"membership":"%s"}' % Membership.JOIN + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("GET", path, None) + self.assertEquals(200, code, msg=str(response)) + + expected_response = { + "membership": Membership.JOIN, + } + self.assertEquals(expected_response, response) + + @defer.inlineCallbacks + def test_rooms_members_other(self): + self.other_id = "@zzsid1:red" + path = "/rooms/%s/state/m.room.member/%s" % ( + urllib.quote(self.room_id), self.other_id + ) + + # valid invite message + content = '{"membership":"%s"}' % Membership.INVITE + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("GET", path, None) + self.assertEquals(200, code, msg=str(response)) + self.assertEquals(json.loads(content), response) + + @defer.inlineCallbacks + def test_rooms_members_other_custom_keys(self): + self.other_id = "@zzsid1:red" + path = "/rooms/%s/state/m.room.member/%s" % ( + urllib.quote(self.room_id), self.other_id + ) + + # valid invite message with custom key + content = ('{"membership":"%s","invite_text":"%s"}' % + (Membership.INVITE, "Join us!")) + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("GET", path, None) + self.assertEquals(200, code, msg=str(response)) + self.assertEquals(json.loads(content), response) + + +class RoomMessagesTestCase(RestTestCase): + """ Tests /rooms/$room_id/messages/$user_id/$msg_id REST events. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + self.room_id = yield self.create_room_as(self.user_id) + + def tearDown(self): + pass + + @defer.inlineCallbacks + def test_invalid_puts(self): + path = "/rooms/%s/send/m.room.message/mid1" % ( + urllib.quote(self.room_id)) + # missing keys or invalid json + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{"_name":"bob"}') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '{"nao') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '[{"_name":"bob"},{"_name":"jill"}]') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, 'text only') + self.assertEquals(400, code, msg=str(response)) + + (code, response) = yield self.mock_resource.trigger("PUT", + path, '') + self.assertEquals(400, code, msg=str(response)) + + @defer.inlineCallbacks + def test_rooms_messages_sent(self): + path = "/rooms/%s/send/m.room.message/mid1" % ( + urllib.quote(self.room_id)) + + content = '{"body":"test","msgtype":{"type":"a"}}' + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(400, code, msg=str(response)) + + # custom message types + content = '{"body":"test","msgtype":"test.custom.text"}' + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + +# (code, response) = yield self.mock_resource.trigger("GET", path, None) +# self.assertEquals(200, code, msg=str(response)) +# self.assert_dict(json.loads(content), response) + + # m.text message type + path = "/rooms/%s/send/m.room.message/mid2" % ( + urllib.quote(self.room_id)) + content = '{"body":"test2","msgtype":"m.text"}' + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(200, code, msg=str(response)) + + +class RoomInitialSyncTestCase(RestTestCase): + """ Tests /rooms/$room_id/initialSync. """ + user_id = "@sid1:red" + + @defer.inlineCallbacks + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + # Since I'm getting my own presence I need to exist as far as presence + # is concerned. + hs.get_handlers().presence_handler.registered_user( + hs.parse_userid(self.user_id) + ) + + # create the room + self.room_id = yield self.create_room_as(self.user_id) + + @defer.inlineCallbacks + def test_initial_sync(self): + (code, response) = yield self.mock_resource.trigger_get( + "/rooms/%s/initialSync" % self.room_id) + self.assertEquals(200, code) + + self.assertEquals(self.room_id, response["room_id"]) + self.assertEquals("join", response["membership"]) + + # Room state is easier to assert on if we unpack it into a dict + state = {} + for event in response["state"]: + if "state_key" not in event: + continue + t = event["type"] + if t not in state: + state[t] = [] + state[t].append(event) + + self.assertTrue("m.room.create" in state) + + self.assertTrue("messages" in response) + self.assertTrue("chunk" in response["messages"]) + self.assertTrue("end" in response["messages"]) + + self.assertTrue("presence" in response) + + presence_by_user = {e["content"]["user_id"]: e + for e in response["presence"] + } + self.assertTrue(self.user_id in presence_by_user) + self.assertEquals("m.presence", presence_by_user[self.user_id]["type"]) diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py new file mode 100644 index 0000000000..647bcebfd8 --- /dev/null +++ b/tests/rest/client/v1/test_typing.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests REST events for /rooms paths.""" + +# twisted imports +from twisted.internet import defer + +import synapse.rest.client.v1.room +from synapse.server import HomeServer + +from ....utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey +from .utils import RestTestCase + +from mock import Mock, NonCallableMock + + +PATH_PREFIX = "/_matrix/client/api/v1" + + +class RoomTypingTestCase(RestTestCase): + """ Tests /rooms/$room_id/typing/$user_id REST API. """ + user_id = "@sid:red" + + @defer.inlineCallbacks + def setUp(self): + self.clock = MockClock() + + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + self.auth_user_id = self.user_id + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + hs = HomeServer( + "red", + clock=self.clock, + db_pool=db_pool, + http_client=None, + replication_layer=Mock(), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=self.mock_config, + ) + self.hs = hs + + self.event_source = hs.get_event_sources().sources["typing"] + + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + + hs.get_handlers().federation_handler = Mock() + + def _get_user_by_token(token=None): + return { + "user": hs.parse_userid(self.auth_user_id), + "admin": False, + "device_id": None, + } + + hs.get_auth().get_user_by_token = _get_user_by_token + + def _insert_client_ip(*args, **kwargs): + return defer.succeed(None) + hs.get_datastore().insert_client_ip = _insert_client_ip + + def get_room_members(room_id): + if room_id == self.room_id: + return defer.succeed([hs.parse_userid(self.user_id)]) + else: + return defer.succeed([]) + + @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 hs.is_mine(member): + if localusers is not None: + localusers.add(member) + else: + if remotedomains is not None: + remotedomains.add(member.domain) + hs.get_handlers().room_member_handler.fetch_room_distributions_into = ( + fetch_room_distributions_into) + + synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) + + self.room_id = yield self.create_room_as(self.user_id) + # Need another user to make notifications actually work + yield self.join(self.room_id, user="@jim:red") + + def tearDown(self): + self.hs.get_handlers().typing_notification_handler.tearDown() + + @defer.inlineCallbacks + def test_set_typing(self): + (code, _) = yield self.mock_resource.trigger("PUT", + "/rooms/%s/typing/%s" % (self.room_id, self.user_id), + '{"typing": true, "timeout": 30000}' + ) + self.assertEquals(200, code) + + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals( + self.event_source.get_new_events_for_user(self.user_id, 0, None)[0], + [ + {"type": "m.typing", + "room_id": self.room_id, + "content": { + "user_ids": [self.user_id], + }}, + ] + ) + + @defer.inlineCallbacks + def test_set_not_typing(self): + (code, _) = yield self.mock_resource.trigger("PUT", + "/rooms/%s/typing/%s" % (self.room_id, self.user_id), + '{"typing": false}' + ) + self.assertEquals(200, code) + + @defer.inlineCallbacks + def test_typing_timeout(self): + (code, _) = yield self.mock_resource.trigger("PUT", + "/rooms/%s/typing/%s" % (self.room_id, self.user_id), + '{"typing": true, "timeout": 30000}' + ) + self.assertEquals(200, code) + + self.assertEquals(self.event_source.get_current_key(), 1) + + self.clock.advance_time(31); + + self.assertEquals(self.event_source.get_current_key(), 2) + + (code, _) = yield self.mock_resource.trigger("PUT", + "/rooms/%s/typing/%s" % (self.room_id, self.user_id), + '{"typing": true, "timeout": 30000}' + ) + self.assertEquals(200, code) + + self.assertEquals(self.event_source.get_current_key(), 3) diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py new file mode 100644 index 0000000000..579441fb4a --- /dev/null +++ b/tests/rest/client/v1/utils.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# twisted imports +from twisted.internet import defer + +# trial imports +from tests import unittest + +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. + + This subclass assumes there are mock_resource and auth_user_id attributes. + """ + + def __init__(self, *args, **kwargs): + super(RestTestCase, self).__init__(*args, **kwargs) + self.mock_resource = None + self.auth_user_id = None + + def mock_get_user_by_token(self, token=None): + return self.auth_user_id + + @defer.inlineCallbacks + 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 = "/createRoom" + content = "{}" + if not is_public: + content = '{"visibility":"private"}' + if tok: + path = path + "?access_token=%s" % tok + (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): + yield self.change_membership(room=room, src=src, targ=targ, tok=tok, + membership=Membership.INVITE, + expect_code=expect_code) + + @defer.inlineCallbacks + def join(self, room=None, user=None, expect_code=200, tok=None): + yield self.change_membership(room=room, src=user, targ=user, tok=tok, + membership=Membership.JOIN, + expect_code=expect_code) + + @defer.inlineCallbacks + def leave(self, room=None, user=None, expect_code=200, tok=None): + yield self.change_membership(room=room, src=user, targ=user, tok=tok, + membership=Membership.LEAVE, + expect_code=expect_code) + + @defer.inlineCallbacks + def change_membership(self, room, src, targ, membership, tok=None, + expect_code=200): + temp_id = self.auth_user_id + self.auth_user_id = src + + path = "/rooms/%s/state/m.room.member/%s" % (room, targ) + if tok: + path = path + "?access_token=%s" % tok + + data = { + "membership": membership + } + + (code, response) = yield self.mock_resource.trigger("PUT", path, + json.dumps(data)) + self.assertEquals(expect_code, code, msg=str(response)) + + self.auth_user_id = temp_id + + @defer.inlineCallbacks + def register(self, user_id): + (code, response) = yield self.mock_resource.trigger( + "POST", + "/register", + json.dumps({ + "user": user_id, + "password": "test", + "type": "m.login.password" + })) + self.assertEquals(200, code) + defer.returnValue(response) + + @defer.inlineCallbacks + def send(self, room_id, body=None, txn_id=None, tok=None, + expect_code=200): + if txn_id is None: + txn_id = "m%s" % (str(time.time())) + if body is None: + body = "body_text_here" + + path = "/rooms/%s/send/m.room.message/%s" % (room_id, txn_id) + content = '{"msgtype":"m.text","body":"%s"}' % body + if tok: + path = path + "?access_token=%s" % tok + + (code, response) = yield self.mock_resource.trigger("PUT", path, content) + self.assertEquals(expect_code, code, msg=str(response)) + + def assert_dict(self, required, actual): + """Does a partial assert of a dict. + + Args: + required (dict): The keys and value which MUST be in 'actual'. + actual (dict): The test result. Extra keys will not be checked. + """ + for key in required: + self.assertEquals(required[key], actual[key], + msg="%s mismatch. %s" % (key, actual)) -- cgit 1.5.1 From cbb10879cb4bd16faf8fc010271bee966e1e6cad Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Thu, 22 Jan 2015 17:54:52 +0000 Subject: Much merging of test case setUp() methods to make them much more shareable --- tests/handlers/test_presence.py | 466 +++++++++++++++++----------------------- 1 file changed, 193 insertions(+), 273 deletions(-) diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index c309fbb054..805f3868bb 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -63,9 +63,6 @@ class JustPresenceHandlers(object): class PresenceTestCase(unittest.TestCase): @defer.inlineCallbacks def setUp(self): - db_pool = SQLiteMemoryDbPool() - yield db_pool.prepare() - self.clock = MockClock() self.mock_config = NonCallableMock() @@ -76,6 +73,15 @@ class PresenceTestCase(unittest.TestCase): self.mock_http_client = Mock(spec=[]) self.mock_http_client.put_json = DeferredMockCallable() + db_pool = None + hs_kwargs = {} + + if hasattr(self, "make_datastore_mock"): + hs_kwargs["datastore"] = self.make_datastore_mock() + else: + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + hs = HomeServer("test", clock=self.clock, db_pool=db_pool, @@ -84,38 +90,33 @@ class PresenceTestCase(unittest.TestCase): http_client=self.mock_http_client, config=self.mock_config, keyring=Mock(), + **hs_kwargs ) hs.handlers = JustPresenceHandlers(hs) - self.store = hs.get_datastore() - - # Mock the RoomMemberHandler - room_member_handler = Mock(spec=[]) - hs.handlers.room_member_handler = room_member_handler - - # Some local users to test with - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - self.u_clementine = hs.parse_userid("@clementine:test") - - for u in self.u_apple, self.u_banana, self.u_clementine: - yield self.store.create_presence(u.localpart) + self.datastore = hs.get_datastore() - yield self.store.set_presence_state( - self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"} - ) + self.setUp_roommemberhandler_mocks(hs.handlers) - # ID of a local user that does not exist - self.u_durian = hs.parse_userid("@durian:test") + self.handler = hs.get_handlers().presence_handler + self.event_source = hs.get_event_sources().sources["presence"] - # A remote user - self.u_cabbage = hs.parse_userid("@cabbage:elsewhere") + self.distributor = hs.get_distributor() + self.distributor.declare("user_joined_room") - self.handler = hs.get_handlers().presence_handler + yield self.setUp_users(hs) + def setUp_roommemberhandler_mocks(self, handlers): self.room_id = "a-room" self.room_members = [] + room_member_handler = handlers.room_member_handler = Mock(spec=[ + "get_rooms_for_user", + "get_room_members", + "fetch_room_distributions_into", + ]) + self.room_member_handler = room_member_handler + def get_rooms_for_user(user): if user in self.room_members: return defer.succeed([self.room_id]) @@ -130,22 +131,150 @@ class PresenceTestCase(unittest.TestCase): return defer.succeed([]) 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) + room_member_handler.fetch_room_distributions_into = ( + fetch_room_distributions_into) + + self.setUp_datastore_room_mocks(self.datastore) + + def setUp_datastore_room_mocks(self, datastore): + def get_room_hosts(room_id): + if room_id == self.room_id: + hosts = set([u.domain for u in self.room_members]) + return defer.succeed(hosts) + else: + return defer.succeed([]) + datastore.get_joined_hosts_for_room = get_room_hosts + def user_rooms_intersect(userlist): room_member_ids = map(lambda u: u.to_string(), self.room_members) shared = all(map(lambda i: i in room_member_ids, userlist)) return defer.succeed(shared) - self.store.user_rooms_intersect = user_rooms_intersect + datastore.user_rooms_intersect = user_rooms_intersect - self.mock_start = Mock() - self.mock_stop = Mock() + @defer.inlineCallbacks + def setUp_users(self, hs): + # Some local users to test with + self.u_apple = hs.parse_userid("@apple:test") + self.u_banana = hs.parse_userid("@banana:test") + self.u_clementine = hs.parse_userid("@clementine:test") - self.handler.start_polling_presence = self.mock_start - self.handler.stop_polling_presence = self.mock_stop + for u in self.u_apple, self.u_banana, self.u_clementine: + yield self.datastore.create_presence(u.localpart) + + yield self.datastore.set_presence_state( + self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"} + ) + + # ID of a local user that does not exist + self.u_durian = hs.parse_userid("@durian:test") + + # A remote user + self.u_cabbage = hs.parse_userid("@cabbage:elsewhere") + + +class MockedDatastoreTestCase(PresenceTestCase): + def make_datastore_mock(self): + datastore = Mock(spec=[ + # Bits that Federation needs + "prep_send_transaction", + "delivered_txn", + "get_received_txn_response", + "set_received_txn_response", + "get_destination_retry_timings", + ]) + + self.setUp_datastore_federation_mocks(datastore) + self.setUp_datastore_presence_mocks(datastore) + + return datastore + + def setUp_datastore_federation_mocks(self, datastore): + datastore.get_destination_retry_timings.return_value = ( + defer.succeed(DestinationsTable.EntryType("", 0, 0)) + ) + + def get_received_txn_response(*args): + return defer.succeed(None) + datastore.get_received_txn_response = get_received_txn_response + + def setUp_datastore_presence_mocks(self, datastore): + self.current_user_state = { + "apple": OFFLINE, + "banana": OFFLINE, + "clementine": OFFLINE, + "fig": OFFLINE, + } + + def get_presence_state(user_localpart): + return defer.succeed( + {"state": self.current_user_state[user_localpart], + "status_msg": None, + "mtime": 123456000} + ) + datastore.get_presence_state = get_presence_state + + def set_presence_state(user_localpart, new_state): + was = self.current_user_state[user_localpart] + self.current_user_state[user_localpart] = new_state["state"] + return defer.succeed({"state": was}) + datastore.set_presence_state = set_presence_state + + def get_presence_list(user_localpart, accepted): + if not user_localpart in self.PRESENCE_LIST: + return defer.succeed([]) + return defer.succeed([ + {"observed_user_id": u} for u in + self.PRESENCE_LIST[user_localpart]]) + datastore.get_presence_list = get_presence_list + + def is_presence_visible(observed_localpart, observer_userid): + return True + datastore.is_presence_visible = is_presence_visible + + @defer.inlineCallbacks + def setUp_users(self, hs): + # Some local users to test with + self.u_apple = hs.parse_userid("@apple:test") + self.u_banana = hs.parse_userid("@banana:test") + self.u_clementine = hs.parse_userid("@clementine:test") + self.u_durian = hs.parse_userid("@durian:test") + self.u_elderberry = hs.parse_userid("@elderberry:test") + self.u_fig = hs.parse_userid("@fig:test") + + # Remote user + self.u_onion = hs.parse_userid("@onion:farm") + self.u_potato = hs.parse_userid("@potato:remote") + + yield class PresenceStateTestCase(PresenceTestCase): """ Tests presence management. """ + @defer.inlineCallbacks + def setUp(self): + yield super(PresenceStateTestCase, self).setUp() + + self.mock_start = Mock() + self.mock_stop = Mock() + + self.handler.start_polling_presence = self.mock_start + self.handler.stop_polling_presence = self.mock_stop @defer.inlineCallbacks def test_get_my_state(self): @@ -160,7 +289,7 @@ class PresenceStateTestCase(PresenceTestCase): @defer.inlineCallbacks def test_get_allowed_state(self): - yield self.store.allow_presence_visible( + yield self.datastore.allow_presence_visible( observed_localpart=self.u_apple.localpart, observer_userid=self.u_banana.to_string(), ) @@ -208,7 +337,7 @@ class PresenceStateTestCase(PresenceTestCase): {"state": UNAVAILABLE, "status_msg": "Away", "mtime": 1000000}, - (yield self.store.get_presence_state(self.u_apple.localpart)) + (yield self.datastore.get_presence_state(self.u_apple.localpart)) ) self.mock_start.assert_called_with(self.u_apple, @@ -227,6 +356,15 @@ class PresenceStateTestCase(PresenceTestCase): class PresenceInvitesTestCase(PresenceTestCase): """ Tests presence management. """ + @defer.inlineCallbacks + def setUp(self): + yield super(PresenceInvitesTestCase, self).setUp() + + self.mock_start = Mock() + self.mock_stop = Mock() + + self.handler.start_polling_presence = self.mock_start + self.handler.stop_polling_presence = self.mock_stop @defer.inlineCallbacks def test_invite_local(self): @@ -238,10 +376,10 @@ class PresenceInvitesTestCase(PresenceTestCase): self.assertEquals( [{"observed_user_id": "@banana:test", "accepted": 1}], - (yield self.store.get_presence_list(self.u_apple.localpart)) + (yield self.datastore.get_presence_list(self.u_apple.localpart)) ) self.assertTrue( - (yield self.store.is_presence_visible( + (yield self.datastore.is_presence_visible( observed_localpart=self.u_banana.localpart, observer_userid=self.u_apple.to_string(), )) @@ -257,7 +395,7 @@ class PresenceInvitesTestCase(PresenceTestCase): self.assertEquals( [], - (yield self.store.get_presence_list(self.u_apple.localpart)) + (yield self.datastore.get_presence_list(self.u_apple.localpart)) ) @defer.inlineCallbacks @@ -282,7 +420,7 @@ class PresenceInvitesTestCase(PresenceTestCase): self.assertEquals( [{"observed_user_id": "@cabbage:elsewhere", "accepted": 0}], - (yield self.store.get_presence_list(self.u_apple.localpart)) + (yield self.datastore.get_presence_list(self.u_apple.localpart)) ) yield put_json.await_calls() @@ -317,7 +455,7 @@ class PresenceInvitesTestCase(PresenceTestCase): ) self.assertTrue( - (yield self.store.is_presence_visible( + (yield self.datastore.is_presence_visible( observed_localpart=self.u_apple.localpart, observer_userid=self.u_cabbage.to_string(), )) @@ -356,7 +494,7 @@ class PresenceInvitesTestCase(PresenceTestCase): @defer.inlineCallbacks def test_accepted_remote(self): - yield self.store.add_presence_list_pending( + yield self.datastore.add_presence_list_pending( observer_localpart=self.u_apple.localpart, observed_userid=self.u_cabbage.to_string(), ) @@ -373,7 +511,7 @@ class PresenceInvitesTestCase(PresenceTestCase): self.assertEquals( [{"observed_user_id": "@cabbage:elsewhere", "accepted": 1}], - (yield self.store.get_presence_list(self.u_apple.localpart)) + (yield self.datastore.get_presence_list(self.u_apple.localpart)) ) self.mock_start.assert_called_with( @@ -381,7 +519,7 @@ class PresenceInvitesTestCase(PresenceTestCase): @defer.inlineCallbacks def test_denied_remote(self): - yield self.store.add_presence_list_pending( + yield self.datastore.add_presence_list_pending( observer_localpart=self.u_apple.localpart, observed_userid="@eggplant:elsewhere", ) @@ -398,16 +536,16 @@ class PresenceInvitesTestCase(PresenceTestCase): self.assertEquals( [], - (yield self.store.get_presence_list(self.u_apple.localpart)) + (yield self.datastore.get_presence_list(self.u_apple.localpart)) ) @defer.inlineCallbacks def test_drop_local(self): - yield self.store.add_presence_list_pending( + yield self.datastore.add_presence_list_pending( observer_localpart=self.u_apple.localpart, observed_userid=self.u_banana.to_string(), ) - yield self.store.set_presence_list_accepted( + yield self.datastore.set_presence_list_accepted( observer_localpart=self.u_apple.localpart, observed_userid=self.u_banana.to_string(), ) @@ -419,7 +557,7 @@ class PresenceInvitesTestCase(PresenceTestCase): self.assertEquals( [], - (yield self.store.get_presence_list(self.u_apple.localpart)) + (yield self.datastore.get_presence_list(self.u_apple.localpart)) ) self.mock_stop.assert_called_with( @@ -427,11 +565,11 @@ class PresenceInvitesTestCase(PresenceTestCase): @defer.inlineCallbacks def test_drop_remote(self): - yield self.store.add_presence_list_pending( + yield self.datastore.add_presence_list_pending( observer_localpart=self.u_apple.localpart, observed_userid=self.u_cabbage.to_string(), ) - yield self.store.set_presence_list_accepted( + yield self.datastore.set_presence_list_accepted( observer_localpart=self.u_apple.localpart, observed_userid=self.u_cabbage.to_string(), ) @@ -443,16 +581,16 @@ class PresenceInvitesTestCase(PresenceTestCase): self.assertEquals( [], - (yield self.store.get_presence_list(self.u_apple.localpart)) + (yield self.datastore.get_presence_list(self.u_apple.localpart)) ) @defer.inlineCallbacks def test_get_presence_list(self): - yield self.store.add_presence_list_pending( + yield self.datastore.add_presence_list_pending( observer_localpart=self.u_apple.localpart, observed_userid=self.u_banana.to_string(), ) - yield self.store.set_presence_list_accepted( + yield self.datastore.set_presence_list_accepted( observer_localpart=self.u_apple.localpart, observed_userid=self.u_banana.to_string(), ) @@ -467,7 +605,7 @@ class PresenceInvitesTestCase(PresenceTestCase): ], presence) -class PresencePushTestCase(unittest.TestCase): +class PresencePushTestCase(MockedDatastoreTestCase): """ Tests steady-state presence status updates. They assert that presence state update messages are pushed around the place @@ -477,139 +615,9 @@ class PresencePushTestCase(unittest.TestCase): presence handler; namely the _local_pushmap and _remote_recvmap. BE WARNED... """ - def setUp(self): - self.clock = MockClock() - - self.mock_http_client = Mock(spec=[]) - self.mock_http_client.put_json = DeferredMockCallable() - - self.mock_federation_resource = MockHttpResource() - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - hs = HomeServer("test", - clock=self.clock, - db_pool=None, - datastore=Mock(spec=[ - "set_presence_state", - "get_joined_hosts_for_room", - - # Bits that Federation needs - "prep_send_transaction", - "delivered_txn", - "get_received_txn_response", - "set_received_txn_response", - "get_destination_retry_timings", - ]), - handlers=None, - resource_for_client=Mock(), - resource_for_federation=self.mock_federation_resource, - http_client=self.mock_http_client, - config=self.mock_config, - keyring=Mock(), - ) - hs.handlers = JustPresenceHandlers(hs) - - self.datastore = hs.get_datastore() - self.datastore.get_destination_retry_timings.return_value = ( - defer.succeed(DestinationsTable.EntryType("", 0, 0)) - ) - - def get_received_txn_response(*args): - return defer.succeed(None) - self.datastore.get_received_txn_response = get_received_txn_response - - self.handler = hs.get_handlers().presence_handler - self.event_source = hs.get_event_sources().sources["presence"] - - # Mock the RoomMemberHandler - hs.handlers.room_member_handler = Mock(spec=[ - "get_rooms_for_user", - "get_room_members", - ]) - self.room_member_handler = hs.handlers.room_member_handler - - self.room_id = "a-room" - 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 - - def get_room_hosts(room_id): - if room_id == self.room_id: - hosts = set([u.domain for u in self.room_members]) - return defer.succeed(hosts) - else: - return defer.succeed([]) - self.datastore.get_joined_hosts_for_room = get_room_hosts - - def user_rooms_intersect(userlist): - room_member_ids = map(lambda u: u.to_string(), self.room_members) - - shared = all(map(lambda i: i in room_member_ids, userlist)) - return defer.succeed(shared) - self.datastore.user_rooms_intersect = user_rooms_intersect - - @defer.inlineCallbacks - def fetch_room_distributions_into(room_id, localusers=None, - remotedomains=None, ignore_user=None): - - 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) - - def get_presence_list(user_localpart, accepted=None): - if user_localpart == "apple": - return defer.succeed([ - {"observed_user_id": "@banana:test"}, - {"observed_user_id": "@clementine:test"}, - ]) - else: - return defer.succeed([]) - self.datastore.get_presence_list = get_presence_list - - def is_presence_visible(observer_userid, observed_localpart): - if (observed_localpart == "clementine" and - observer_userid == "@banana:test"): - return False - return False - self.datastore.is_presence_visible = is_presence_visible - - self.distributor = hs.get_distributor() - self.distributor.declare("user_joined_room") - - # Some local users to test with - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - self.u_clementine = hs.parse_userid("@clementine:test") - self.u_durian = hs.parse_userid("@durian:test") - self.u_elderberry = hs.parse_userid("@elderberry:test") - - # Remote user - self.u_onion = hs.parse_userid("@onion:farm") - self.u_potato = hs.parse_userid("@potato:remote") + PRESENCE_LIST = { + 'apple': [ "@banana:test", "@clementine:test" ], + } @defer.inlineCallbacks def test_push_local(self): @@ -982,7 +990,7 @@ class PresencePushTestCase(unittest.TestCase): put_json.await_calls() -class PresencePollingTestCase(unittest.TestCase): +class PresencePollingTestCase(MockedDatastoreTestCase): """ Tests presence status polling. """ # For this test, we have three local users; apple is watching and is @@ -995,106 +1003,18 @@ class PresencePollingTestCase(unittest.TestCase): 'fig': [ "@potato:remote" ], } - + @defer.inlineCallbacks def setUp(self): - self.mock_http_client = Mock(spec=[]) - self.mock_http_client.put_json = DeferredMockCallable() - - self.mock_federation_resource = MockHttpResource() - - self.mock_config = NonCallableMock() - self.mock_config.signing_key = [MockKey()] - - hs = HomeServer("test", - clock=MockClock(), - db_pool=None, - datastore=Mock(spec=[ - # Bits that Federation needs - "prep_send_transaction", - "delivered_txn", - "get_received_txn_response", - "set_received_txn_response", - "get_destination_retry_timings", - ]), - handlers=None, - resource_for_client=Mock(), - resource_for_federation=self.mock_federation_resource, - http_client=self.mock_http_client, - config=self.mock_config, - keyring=Mock(), - ) - hs.handlers = JustPresenceHandlers(hs) - - self.datastore = hs.get_datastore() - self.datastore.get_destination_retry_timings.return_value = ( - defer.succeed(DestinationsTable.EntryType("", 0, 0)) - ) - - def get_received_txn_response(*args): - return defer.succeed(None) - self.datastore.get_received_txn_response = get_received_txn_response + yield super(PresencePollingTestCase, self).setUp() self.mock_update_client = Mock() def update(*args,**kwargs): - # print "mock_update_client: Args=%s, kwargs=%s" %(args, kwargs,) return defer.succeed(None) - self.mock_update_client.side_effect = update - self.handler = hs.get_handlers().presence_handler self.handler.push_update_to_clients = self.mock_update_client - hs.handlers.room_member_handler = Mock(spec=[ - "get_rooms_for_user", - ]) - # For this test no users are ever in rooms - def get_rooms_for_user(user): - return defer.succeed([]) - hs.handlers.room_member_handler.get_rooms_for_user = get_rooms_for_user - - # Mocked database state - # Local users always start offline - self.current_user_state = { - "apple": OFFLINE, - "banana": OFFLINE, - "clementine": OFFLINE, - "fig": OFFLINE, - } - - def get_presence_state(user_localpart): - return defer.succeed( - {"state": self.current_user_state[user_localpart], - "status_msg": None, - "mtime": 123456000} - ) - self.datastore.get_presence_state = get_presence_state - - def set_presence_state(user_localpart, new_state): - was = self.current_user_state[user_localpart] - self.current_user_state[user_localpart] = new_state["state"] - return defer.succeed({"state": was}) - self.datastore.set_presence_state = set_presence_state - - def get_presence_list(user_localpart, accepted): - return defer.succeed([ - {"observed_user_id": u} for u in - self.PRESENCE_LIST[user_localpart]]) - self.datastore.get_presence_list = get_presence_list - - def is_presence_visible(observed_localpart, observer_userid): - return True - self.datastore.is_presence_visible = is_presence_visible - - # Local users - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - self.u_clementine = hs.parse_userid("@clementine:test") - self.u_fig = hs.parse_userid("@fig:test") - - # Remote users - self.u_potato = hs.parse_userid("@potato:remote") - @defer.inlineCallbacks def test_push_local(self): # apple goes online -- cgit 1.5.1 From 3a243c53f41a719c4de62630db6018f97d5b94ae Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Thu, 22 Jan 2015 20:06:08 +0000 Subject: Rename MockedDatastoreTestCase to MockedDatastorePresenceTestCase since it is still presence-specific --- tests/handlers/test_presence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 805f3868bb..56e90177f1 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -188,7 +188,7 @@ class PresenceTestCase(unittest.TestCase): self.u_cabbage = hs.parse_userid("@cabbage:elsewhere") -class MockedDatastoreTestCase(PresenceTestCase): +class MockedDatastorePresenceTestCase(PresenceTestCase): def make_datastore_mock(self): datastore = Mock(spec=[ # Bits that Federation needs @@ -605,7 +605,7 @@ class PresenceInvitesTestCase(PresenceTestCase): ], presence) -class PresencePushTestCase(MockedDatastoreTestCase): +class PresencePushTestCase(MockedDatastorePresenceTestCase): """ Tests steady-state presence status updates. They assert that presence state update messages are pushed around the place @@ -990,7 +990,7 @@ class PresencePushTestCase(MockedDatastoreTestCase): put_json.await_calls() -class PresencePollingTestCase(MockedDatastoreTestCase): +class PresencePollingTestCase(MockedDatastorePresenceTestCase): """ Tests presence status polling. """ # For this test, we have three local users; apple is watching and is -- cgit 1.5.1 From 7256def8e43bf5ab982cb7e785fb1334a1ef4ab8 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 23 Jan 2015 10:37:38 +0000 Subject: Merge rest servlets into the client json resource object --- synapse/app/homeserver.py | 5 ++--- synapse/rest/client/v1/__init__.py | 18 +++++++----------- synapse/server.py | 10 ---------- tests/rest/client/v1/test_presence.py | 9 ++++++--- tests/rest/client/v1/test_profile.py | 4 +++- 5 files changed, 18 insertions(+), 28 deletions(-) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index cd24bbdc79..fabe8ddacb 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -37,6 +37,7 @@ from synapse.api.urls import ( from synapse.config.homeserver import HomeServerConfig from synapse.crypto import context_factory from synapse.util.logcontext import LoggingContext +from synapse.rest.client.v1 import ClientV1RestResource from daemonize import Daemonize import twisted.manhole.telnet @@ -59,7 +60,7 @@ class SynapseHomeServer(HomeServer): return MatrixFederationHttpClient(self) def build_resource_for_client(self): - return JsonResource() + return ClientV1RestResource(self) def build_resource_for_federation(self): return JsonResource() @@ -224,8 +225,6 @@ def setup(): content_addr=config.content_addr, ) - hs.register_servlets() - hs.create_resource_tree( web_client=config.webclient, redirect_root_to_web_client=True, diff --git a/synapse/rest/client/v1/__init__.py b/synapse/rest/client/v1/__init__.py index 88ec9cd27d..8bb89b2f6a 100644 --- a/synapse/rest/client/v1/__init__.py +++ b/synapse/rest/client/v1/__init__.py @@ -19,22 +19,18 @@ from . import ( voip, admin, ) +from synapse.http.server import JsonResource -class RestServletFactory(object): - """ A factory for creating REST servlets. - - These REST servlets represent the entire client-server REST API. Generally - speaking, they serve as wrappers around events and the handlers that - process them. - - See synapse.events for information on synapse events. - """ +class ClientV1RestResource(JsonResource): + """A resource for version 1 of the matrix client API.""" def __init__(self, hs): - client_resource = hs.get_resource_for_client() + JsonResource.__init__(self) + self.register_servlets(self, hs) - # TODO(erikj): There *must* be a better way of doing this. + @staticmethod + def register_servlets(client_resource, hs): room.register_servlets(hs, client_resource) events.register_servlets(hs, client_resource) register.register_servlets(hs, client_resource) diff --git a/synapse/server.py b/synapse/server.py index e9add8e2b4..476d809374 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -24,7 +24,6 @@ from synapse.events.utils import serialize_event from synapse.notifier import Notifier from synapse.api.auth import Auth from synapse.handlers import Handlers -from synapse.rest.client.v1 import RestServletFactory from synapse.state import StateHandler from synapse.storage import DataStore from synapse.types import UserID, RoomAlias, RoomID, EventID @@ -203,9 +202,6 @@ class HomeServer(BaseHomeServer): def build_auth(self): return Auth(self) - def build_rest_servlet_factory(self): - return RestServletFactory(self) - def build_state_handler(self): return StateHandler(self) @@ -229,9 +225,3 @@ class HomeServer(BaseHomeServer): clock=self.get_clock(), hostname=self.hostname, ) - - def register_servlets(self): - """ Register all servlets associated with this HomeServer. - """ - # Simply building the ServletFactory is sufficient to have it register - self.get_rest_servlet_factory() diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index 0b6f7cfccb..783720ac29 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -25,6 +25,8 @@ from ....utils import MockHttpResource, MockKey from synapse.api.constants import PresenceState from synapse.handlers.presence import PresenceHandler from synapse.server import HomeServer +from synapse.rest.client.v1 import presence +from synapse.rest.client.v1 import events OFFLINE = PresenceState.OFFLINE @@ -86,7 +88,7 @@ class PresenceStateTestCase(unittest.TestCase): return defer.succeed([]) room_member_handler.get_rooms_for_user = get_rooms_for_user - hs.register_servlets() + presence.register_servlets(hs, self.mock_resource) self.u_apple = hs.parse_userid(myid) @@ -172,7 +174,7 @@ class PresenceListTestCase(unittest.TestCase): hs.get_auth().get_user_by_token = _get_user_by_token - hs.register_servlets() + presence.register_servlets(hs, self.mock_resource) self.u_apple = hs.parse_userid("@apple:test") self.u_banana = hs.parse_userid("@banana:test") @@ -283,7 +285,8 @@ class PresenceEventStreamTestCase(unittest.TestCase): hs.get_auth().get_user_by_req = _get_user_by_req - hs.register_servlets() + presence.register_servlets(hs, self.mock_resource) + events.register_servlets(hs, self.mock_resource) hs.handlers.room_member_handler = Mock(spec=[]) diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py index 47cfb10a6d..5b5c3edc22 100644 --- a/tests/rest/client/v1/test_profile.py +++ b/tests/rest/client/v1/test_profile.py @@ -25,6 +25,8 @@ from ....utils import MockHttpResource, MockKey from synapse.api.errors import SynapseError, AuthError from synapse.server import HomeServer +from synapse.rest.client.v1 import profile + myid = "@1234ABCD:test" PATH_PREFIX = "/_matrix/client/api/v1" @@ -61,7 +63,7 @@ class ProfileTestCase(unittest.TestCase): hs.get_handlers().profile_handler = self.mock_handler - hs.register_servlets() + profile.register_servlets(hs, self.mock_resource) @defer.inlineCallbacks def test_get_my_name(self): -- cgit 1.5.1 From 5759bec43cb52862a8d455afb8cd9d1c5660bc3d Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 23 Jan 2015 11:47:15 +0000 Subject: Replace hs.parse_userid with UserID.from_string --- synapse/api/auth.py | 9 +++++---- synapse/handlers/_base.py | 5 +++-- synapse/handlers/events.py | 3 ++- synapse/handlers/federation.py | 13 +++++++------ synapse/handlers/message.py | 13 +++++++------ synapse/handlers/presence.py | 23 ++++++++++++----------- synapse/handlers/profile.py | 3 ++- synapse/handlers/room.py | 14 +++++++------- synapse/handlers/typing.py | 3 ++- synapse/rest/client/v1/admin.py | 4 +++- synapse/rest/client/v1/presence.py | 15 ++++++++------- synapse/rest/client/v1/profile.py | 13 +++++++------ synapse/rest/client/v1/room.py | 5 +++-- synapse/server.py | 6 ------ synapse/storage/roommember.py | 5 +++-- tests/handlers/test_presence.py | 29 +++++++++++++++-------------- tests/handlers/test_presencelike.py | 9 +++++---- tests/handlers/test_profile.py | 8 ++++---- tests/handlers/test_room.py | 9 +++++---- tests/handlers/test_typing.py | 7 ++++--- tests/rest/client/v1/test_presence.py | 19 ++++++++++--------- tests/rest/client/v1/test_profile.py | 3 ++- tests/rest/client/v1/test_rooms.py | 21 +++++++++------------ tests/rest/client/v1/test_typing.py | 5 +++-- tests/storage/test_presence.py | 5 +++-- tests/storage/test_profile.py | 3 ++- tests/storage/test_redaction.py | 5 +++-- tests/storage/test_room.py | 3 ++- tests/storage/test_roommember.py | 7 ++++--- tests/storage/test_stream.py | 5 +++-- tests/test_types.py | 6 ------ 31 files changed, 145 insertions(+), 133 deletions(-) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index e31482cfaa..a342a0e0da 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -21,6 +21,7 @@ from synapse.api.constants import EventTypes, Membership, JoinRules from synapse.api.errors import AuthError, StoreError, Codes, SynapseError from synapse.util.logutils import log_function from synapse.util.async import run_on_reactor +from synapse.types import UserID import logging @@ -104,7 +105,7 @@ class Auth(object): for event in curr_state: if event.type == EventTypes.Member: try: - if self.hs.parse_userid(event.state_key).domain != host: + if UserID.from_string(event.state_key).domain != host: continue except: logger.warn("state_key not user_id: %s", event.state_key) @@ -337,7 +338,7 @@ class Auth(object): user_info = { "admin": bool(ret.get("admin", False)), "device_id": ret.get("device_id"), - "user": self.hs.parse_userid(ret.get("name")), + "user": UserID.from_string(ret.get("name")), } defer.returnValue(user_info) @@ -461,7 +462,7 @@ class Auth(object): "You are not allowed to set others state" ) else: - sender_domain = self.hs.parse_userid( + sender_domain = UserID.from_string( event.user_id ).domain @@ -496,7 +497,7 @@ class Auth(object): # Validate users for k, v in user_list.items(): try: - self.hs.parse_userid(k) + UserID.from_string(k) except: raise SynapseError(400, "Not a valid user_id: %s" % (k,)) diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index f33d17a31e..1773fa20aa 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -19,6 +19,7 @@ from synapse.api.errors import LimitExceededError, SynapseError from synapse.util.async import run_on_reactor from synapse.crypto.event_signing import add_hashes_and_signatures from synapse.api.constants import Membership, EventTypes +from synapse.types import UserID import logging @@ -113,7 +114,7 @@ class BaseHandler(object): if event.type == EventTypes.Member: if event.content["membership"] == Membership.INVITE: - invitee = self.hs.parse_userid(event.state_key) + invitee = UserID.from_string(event.state_key) if not self.hs.is_mine(invitee): # TODO: Can we add signature from remote server in a nicer # way? If we have been invited by a remote server, we need @@ -134,7 +135,7 @@ class BaseHandler(object): if k[0] == EventTypes.Member: if s.content["membership"] == Membership.JOIN: destinations.add( - self.hs.parse_userid(s.state_key).domain + UserID.from_string(s.state_key).domain ) except SynapseError: logger.warn( diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index 103bc67c42..01e67b0818 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -17,6 +17,7 @@ from twisted.internet import defer from synapse.util.logcontext import PreserveLoggingContext from synapse.util.logutils import log_function +from synapse.types import UserID from ._base import BaseHandler @@ -48,7 +49,7 @@ class EventStreamHandler(BaseHandler): @log_function def get_stream(self, auth_user_id, pagin_config, timeout=0, as_client_event=True): - auth_user = self.hs.parse_userid(auth_user_id) + auth_user = UserID.from_string(auth_user_id) try: if auth_user not in self._streams_per_user: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 81203bf1a3..bcdcc90a18 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -28,6 +28,7 @@ from synapse.crypto.event_signing import ( compute_event_signature, check_event_content_hash, add_hashes_and_signatures, ) +from synapse.types import UserID from syutil.jsonutil import encode_canonical_json from twisted.internet import defer @@ -227,7 +228,7 @@ class FederationHandler(BaseHandler): extra_users = [] if event.type == EventTypes.Member: target_user_id = event.state_key - target_user = self.hs.parse_userid(target_user_id) + target_user = UserID.from_string(target_user_id) extra_users.append(target_user) yield self.notifier.on_new_room_event( @@ -236,7 +237,7 @@ class FederationHandler(BaseHandler): if event.type == EventTypes.Member: if event.membership == Membership.JOIN: - user = self.hs.parse_userid(event.state_key) + user = UserID.from_string(event.state_key) yield self.distributor.fire( "user_joined_room", user=user, room_id=event.room_id ) @@ -491,7 +492,7 @@ class FederationHandler(BaseHandler): extra_users = [] if event.type == EventTypes.Member: target_user_id = event.state_key - target_user = self.hs.parse_userid(target_user_id) + target_user = UserID.from_string(target_user_id) extra_users.append(target_user) yield self.notifier.on_new_room_event( @@ -500,7 +501,7 @@ class FederationHandler(BaseHandler): if event.type == EventTypes.Member: if event.content["membership"] == Membership.JOIN: - user = self.hs.parse_userid(event.state_key) + user = UserID.from_string(event.state_key) yield self.distributor.fire( "user_joined_room", user=user, room_id=event.room_id ) @@ -514,7 +515,7 @@ class FederationHandler(BaseHandler): if k[0] == EventTypes.Member: if s.content["membership"] == Membership.JOIN: destinations.add( - self.hs.parse_userid(s.state_key).domain + UserID.from_string(s.state_key).domain ) except: logger.warn( @@ -565,7 +566,7 @@ class FederationHandler(BaseHandler): backfilled=False, ) - target_user = self.hs.parse_userid(event.state_key) + target_user = UserID.from_string(event.state_key) yield self.notifier.on_new_room_event( event, extra_users=[target_user], ) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index f2a2f16933..6a1104a890 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -20,6 +20,7 @@ from synapse.api.errors import RoomError from synapse.streams.config import PaginationConfig from synapse.events.validator import EventValidator from synapse.util.logcontext import PreserveLoggingContext +from synapse.types import UserID from ._base import BaseHandler @@ -89,7 +90,7 @@ class MessageHandler(BaseHandler): yield self.hs.get_event_sources().get_current_token() ) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) events, next_key = yield data_source.get_pagination_rows( user, pagin_config.get_source_config("room"), room_id @@ -130,13 +131,13 @@ class MessageHandler(BaseHandler): if ratelimit: self.ratelimit(builder.user_id) # TODO(paul): Why does 'event' not have a 'user' object? - user = self.hs.parse_userid(builder.user_id) + user = UserID.from_string(builder.user_id) assert self.hs.is_mine(user), "User must be our own: %s" % (user,) if builder.type == EventTypes.Member: membership = builder.content.get("membership", None) if membership == Membership.JOIN: - joinee = self.hs.parse_userid(builder.state_key) + joinee = UserID.from_string(builder.state_key) # If event doesn't include a display name, add one. yield self.distributor.fire( "collect_presencelike_data", @@ -237,7 +238,7 @@ class MessageHandler(BaseHandler): membership_list=[Membership.INVITE, Membership.JOIN] ) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) rooms_ret = [] @@ -316,7 +317,7 @@ class MessageHandler(BaseHandler): # TODO(paul): I wish I was called with user objects not user_id # strings... - auth_user = self.hs.parse_userid(user_id) + auth_user = UserID.from_string(user_id) # TODO: These concurrently state_tuples = yield self.state_handler.get_current_state(room_id) @@ -349,7 +350,7 @@ class MessageHandler(BaseHandler): for m in room_members: try: member_presence = yield presence_handler.get_state( - target_user=self.hs.parse_userid(m.user_id), + target_user=UserID.from_string(m.user_id), auth_user=auth_user, as_event=True, ) diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 8aeed99274..d66bfea7b1 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -20,6 +20,7 @@ from synapse.api.constants import PresenceState from synapse.util.logutils import log_function from synapse.util.logcontext import PreserveLoggingContext +from synapse.types import UserID from ._base import BaseHandler @@ -96,22 +97,22 @@ class PresenceHandler(BaseHandler): self.federation.register_edu_handler( "m.presence_invite", lambda origin, content: self.invite_presence( - observed_user=hs.parse_userid(content["observed_user"]), - observer_user=hs.parse_userid(content["observer_user"]), + observed_user=UserID.from_string(content["observed_user"]), + observer_user=UserID.from_string(content["observer_user"]), ) ) self.federation.register_edu_handler( "m.presence_accept", lambda origin, content: self.accept_presence( - observed_user=hs.parse_userid(content["observed_user"]), - observer_user=hs.parse_userid(content["observer_user"]), + observed_user=UserID.from_string(content["observed_user"]), + observer_user=UserID.from_string(content["observer_user"]), ) ) self.federation.register_edu_handler( "m.presence_deny", lambda origin, content: self.deny_presence( - observed_user=hs.parse_userid(content["observed_user"]), - observer_user=hs.parse_userid(content["observer_user"]), + observed_user=UserID.from_string(content["observed_user"]), + observer_user=UserID.from_string(content["observer_user"]), ) ) @@ -418,7 +419,7 @@ class PresenceHandler(BaseHandler): ) for p in presence: - observed_user = self.hs.parse_userid(p.pop("observed_user_id")) + observed_user = UserID.from_string(p.pop("observed_user_id")) p["observed_user"] = observed_user p.update(self._get_or_offline_usercache(observed_user).get_state()) if "last_active" in p: @@ -441,7 +442,7 @@ class PresenceHandler(BaseHandler): user.localpart, accepted=True ) target_users = set([ - self.hs.parse_userid(x["observed_user_id"]) for x in presence + UserID.from_string(x["observed_user_id"]) for x in presence ]) # Also include people in all my rooms @@ -646,7 +647,7 @@ class PresenceHandler(BaseHandler): deferreds = [] for push in content.get("push", []): - user = self.hs.parse_userid(push["user_id"]) + user = UserID.from_string(push["user_id"]) logger.debug("Incoming presence update from %s", user) @@ -694,7 +695,7 @@ class PresenceHandler(BaseHandler): del self._user_cachemap[user] for poll in content.get("poll", []): - user = self.hs.parse_userid(poll) + user = UserID.from_string(poll) if not self.hs.is_mine(user): continue @@ -709,7 +710,7 @@ class PresenceHandler(BaseHandler): deferreds.append(self._push_presence_remote(user, origin)) for unpoll in content.get("unpoll", []): - user = self.hs.parse_userid(unpoll) + user = UserID.from_string(unpoll) if not self.hs.is_mine(user): continue diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 7777d3cc94..03b2159c53 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -18,6 +18,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError, AuthError, CodeMessageException from synapse.api.constants import EventTypes, Membership from synapse.util.logcontext import PreserveLoggingContext +from synapse.types import UserID from ._base import BaseHandler @@ -169,7 +170,7 @@ class ProfileHandler(BaseHandler): @defer.inlineCallbacks def on_profile_query(self, args): - user = self.hs.parse_userid(args["user_id"]) + user = UserID.from_string(args["user_id"]) if not self.hs.is_mine(user): raise SynapseError(400, "User is not hosted on this Home Server") diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 6d0db18e51..0242288c4e 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -64,7 +64,7 @@ class RoomCreationHandler(BaseHandler): invite_list = config.get("invite", []) for i in invite_list: try: - self.hs.parse_userid(i) + UserID.from_string(i) except: raise SynapseError(400, "Invalid user_id: %s" % (i,)) @@ -114,7 +114,7 @@ class RoomCreationHandler(BaseHandler): servers=[self.hs.hostname], ) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) creation_events = self._create_events_for_new_room( user, room_id, is_public=is_public ) @@ -250,7 +250,7 @@ class RoomMemberHandler(BaseHandler): users = yield self.store.get_users_in_room(room_id) - defer.returnValue([hs.parse_userid(u) for u in users]) + defer.returnValue([UserID.from_string(u) for u in users]) @defer.inlineCallbacks def fetch_room_distributions_into(self, room_id, localusers=None, @@ -368,7 +368,7 @@ class RoomMemberHandler(BaseHandler): ) if prev_state and prev_state.membership == Membership.JOIN: - user = self.hs.parse_userid(event.user_id) + user = UserID.from_string(event.user_id) self.distributor.fire( "user_left_room", user=user, room_id=event.room_id ) @@ -412,7 +412,7 @@ class RoomMemberHandler(BaseHandler): @defer.inlineCallbacks def _do_join(self, event, context, room_host=None, do_auth=True): - joinee = self.hs.parse_userid(event.state_key) + joinee = UserID.from_string(event.state_key) # room_id = RoomID.from_string(event.room_id, self.hs) room_id = event.room_id @@ -476,7 +476,7 @@ class RoomMemberHandler(BaseHandler): do_auth=do_auth, ) - user = self.hs.parse_userid(event.user_id) + user = UserID.from_string(event.user_id) yield self.distributor.fire( "user_joined_room", user=user, room_id=room_id ) @@ -526,7 +526,7 @@ class RoomMemberHandler(BaseHandler): do_auth): yield run_on_reactor() - target_user = self.hs.parse_userid(event.state_key) + target_user = UserID.from_string(event.state_key) yield self.handle_new_client_event( event, diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index cd9638dd04..c69787005f 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -18,6 +18,7 @@ from twisted.internet import defer from ._base import BaseHandler from synapse.api.errors import SynapseError, AuthError +from synapse.types import UserID import logging @@ -185,7 +186,7 @@ class TypingNotificationHandler(BaseHandler): @defer.inlineCallbacks def _recv_edu(self, origin, content): room_id = content["room_id"] - user = self.homeserver.parse_userid(content["user_id"]) + user = UserID.from_string(content["user_id"]) localusers = set() diff --git a/synapse/rest/client/v1/admin.py b/synapse/rest/client/v1/admin.py index 0aa83514c8..4aefb94053 100644 --- a/synapse/rest/client/v1/admin.py +++ b/synapse/rest/client/v1/admin.py @@ -16,6 +16,8 @@ from twisted.internet import defer from synapse.api.errors import AuthError, SynapseError +from synapse.types import UserID + from base import RestServlet, client_path_pattern import logging @@ -28,7 +30,7 @@ class WhoisRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): - target_user = self.hs.parse_userid(user_id) + target_user = UserID.from_string(user_id) auth_user = yield self.auth.get_user_by_req(request) is_admin = yield self.auth.is_server_admin(auth_user) diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/v1/presence.py index ca4d2d21f0..22fcb7d7d0 100644 --- a/synapse/rest/client/v1/presence.py +++ b/synapse/rest/client/v1/presence.py @@ -18,7 +18,8 @@ from twisted.internet import defer from synapse.api.errors import SynapseError -from base import RestServlet, client_path_pattern +from synapse.types import UserID +from .base import RestServlet, client_path_pattern import json import logging @@ -32,7 +33,7 @@ class PresenceStatusRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) state = yield self.handlers.presence_handler.get_state( target_user=user, auth_user=auth_user) @@ -42,7 +43,7 @@ class PresenceStatusRestServlet(RestServlet): @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) state = {} try: @@ -77,7 +78,7 @@ class PresenceListRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) if not self.hs.is_mine(user): raise SynapseError(400, "User not hosted on this Home Server") @@ -97,7 +98,7 @@ class PresenceListRestServlet(RestServlet): @defer.inlineCallbacks def on_POST(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) if not self.hs.is_mine(user): raise SynapseError(400, "User not hosted on this Home Server") @@ -118,7 +119,7 @@ class PresenceListRestServlet(RestServlet): raise SynapseError(400, "Bad invite value.") if len(u) == 0: continue - invited_user = self.hs.parse_userid(u) + invited_user = UserID.from_string(u) yield self.handlers.presence_handler.send_invite( observer_user=user, observed_user=invited_user ) @@ -129,7 +130,7 @@ class PresenceListRestServlet(RestServlet): raise SynapseError(400, "Bad drop value.") if len(u) == 0: continue - dropped_user = self.hs.parse_userid(u) + dropped_user = UserID.from_string(u) yield self.handlers.presence_handler.drop( observer_user=user, observed_user=dropped_user ) diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py index dc6eb424b0..39297930c8 100644 --- a/synapse/rest/client/v1/profile.py +++ b/synapse/rest/client/v1/profile.py @@ -16,7 +16,8 @@ """ This module contains REST servlets to do with profile: /profile/ """ from twisted.internet import defer -from base import RestServlet, client_path_pattern +from .base import RestServlet, client_path_pattern +from synapse.types import UserID import json @@ -26,7 +27,7 @@ class ProfileDisplaynameRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) displayname = yield self.handlers.profile_handler.get_displayname( user, @@ -37,7 +38,7 @@ class ProfileDisplaynameRestServlet(RestServlet): @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) try: content = json.loads(request.content.read()) @@ -59,7 +60,7 @@ class ProfileAvatarURLRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) avatar_url = yield self.handlers.profile_handler.get_avatar_url( user, @@ -70,7 +71,7 @@ class ProfileAvatarURLRestServlet(RestServlet): @defer.inlineCallbacks def on_PUT(self, request, user_id): auth_user = yield self.auth.get_user_by_req(request) - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) try: content = json.loads(request.content.read()) @@ -92,7 +93,7 @@ class ProfileRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request, user_id): - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) displayname = yield self.handlers.profile_handler.get_displayname( user, diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 48bba2a5f3..c5837b3403 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -20,6 +20,7 @@ from base import RestServlet, client_path_pattern from synapse.api.errors import SynapseError, Codes from synapse.streams.config import PaginationConfig from synapse.api.constants import EventTypes, Membership +from synapse.types import UserID import json import logging @@ -289,7 +290,7 @@ class RoomMemberListRestServlet(RestServlet): for event in members["chunk"]: # FIXME: should probably be state_key here, not user_id - target_user = self.hs.parse_userid(event["user_id"]) + target_user = UserID.from_string(event["user_id"]) # Presence is an optional cache; don't fail if we can't fetch it try: presence_handler = self.handlers.presence_handler @@ -478,7 +479,7 @@ class RoomTypingRestServlet(RestServlet): auth_user = yield self.auth.get_user_by_req(request) room_id = urllib.unquote(room_id) - target_user = self.hs.parse_userid(urllib.unquote(user_id)) + target_user = UserID.from_string(urllib.unquote(user_id)) content = _parse_json(request) diff --git a/synapse/server.py b/synapse/server.py index 476d809374..52a21aaf78 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -127,12 +127,6 @@ class BaseHomeServer(object): # 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) - def parse_roomalias(self, s): """Parse the string given by 's' as a Room Alias and return a RoomAlias object.""" diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index e59e65529b..c69dd995ce 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -20,6 +20,7 @@ from collections import namedtuple from ._base import SQLBaseStore from synapse.api.constants import Membership +from synapse.types import UserID import logging @@ -39,7 +40,7 @@ class RoomMemberStore(SQLBaseStore): """ try: target_user_id = event.state_key - domain = self.hs.parse_userid(target_user_id).domain + domain = UserID.from_string(target_user_id).domain except: logger.exception( "Failed to parse target_user_id=%s", target_user_id @@ -84,7 +85,7 @@ class RoomMemberStore(SQLBaseStore): for e in member_events: try: joined_domains.add( - self.hs.parse_userid(e.state_key).domain + UserID.from_string(e.state_key).domain ) except: # FIXME: How do we deal with invalid user ids in the db? diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 56e90177f1..5621a8afaf 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -17,7 +17,7 @@ from tests import unittest from twisted.internet import defer, reactor -from mock import Mock, call, ANY, NonCallableMock, patch +from mock import Mock, call, ANY, NonCallableMock import json from tests.utils import ( @@ -31,6 +31,7 @@ from synapse.api.errors import SynapseError from synapse.handlers.presence import PresenceHandler, UserPresenceCache from synapse.streams.config import SourcePaginationConfig from synapse.storage.transactions import DestinationsTable +from synapse.types import UserID OFFLINE = PresenceState.OFFLINE UNAVAILABLE = PresenceState.UNAVAILABLE @@ -170,9 +171,9 @@ class PresenceTestCase(unittest.TestCase): @defer.inlineCallbacks def setUp_users(self, hs): # Some local users to test with - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - self.u_clementine = hs.parse_userid("@clementine:test") + self.u_apple = UserID.from_string("@apple:test") + self.u_banana = UserID.from_string("@banana:test") + self.u_clementine = UserID.from_string("@clementine:test") for u in self.u_apple, self.u_banana, self.u_clementine: yield self.datastore.create_presence(u.localpart) @@ -182,10 +183,10 @@ class PresenceTestCase(unittest.TestCase): ) # ID of a local user that does not exist - self.u_durian = hs.parse_userid("@durian:test") + self.u_durian = UserID.from_string("@durian:test") # A remote user - self.u_cabbage = hs.parse_userid("@cabbage:elsewhere") + self.u_cabbage = UserID.from_string("@cabbage:elsewhere") class MockedDatastorePresenceTestCase(PresenceTestCase): @@ -250,16 +251,16 @@ class MockedDatastorePresenceTestCase(PresenceTestCase): @defer.inlineCallbacks def setUp_users(self, hs): # Some local users to test with - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - self.u_clementine = hs.parse_userid("@clementine:test") - self.u_durian = hs.parse_userid("@durian:test") - self.u_elderberry = hs.parse_userid("@elderberry:test") - self.u_fig = hs.parse_userid("@fig:test") + self.u_apple = UserID.from_string("@apple:test") + self.u_banana = UserID.from_string("@banana:test") + self.u_clementine = UserID.from_string("@clementine:test") + self.u_durian = UserID.from_string("@durian:test") + self.u_elderberry = UserID.from_string("@elderberry:test") + self.u_fig = UserID.from_string("@fig:test") # Remote user - self.u_onion = hs.parse_userid("@onion:farm") - self.u_potato = hs.parse_userid("@potato:remote") + self.u_onion = UserID.from_string("@onion:farm") + self.u_potato = UserID.from_string("@potato:remote") yield diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py index 0584e4c8b9..3cdbb186ae 100644 --- a/tests/handlers/test_presencelike.py +++ b/tests/handlers/test_presencelike.py @@ -27,6 +27,7 @@ from synapse.server import HomeServer from synapse.api.constants import PresenceState from synapse.handlers.presence import PresenceHandler from synapse.handlers.profile import ProfileHandler +from synapse.types import UserID OFFLINE = PresenceState.OFFLINE @@ -136,12 +137,12 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase): lambda u: defer.succeed([])) # Some local users to test with - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") - self.u_clementine = hs.parse_userid("@clementine:test") + self.u_apple = UserID.from_string("@apple:test") + self.u_banana = UserID.from_string("@banana:test") + self.u_clementine = UserID.from_string("@clementine:test") # Remote user - self.u_potato = hs.parse_userid("@potato:remote") + self.u_potato = UserID.from_string("@potato:remote") self.mock_get_joined = ( self.datastore.get_rooms_for_user_where_membership_is diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index 25b172aa5e..7b9590c110 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -22,7 +22,7 @@ from mock import Mock, NonCallableMock from synapse.api.errors import AuthError from synapse.server import HomeServer from synapse.handlers.profile import ProfileHandler -from synapse.api.constants import Membership +from synapse.types import UserID from tests.utils import SQLiteMemoryDbPool, MockKey @@ -71,9 +71,9 @@ class ProfileTestCase(unittest.TestCase): self.store = hs.get_datastore() - self.frank = hs.parse_userid("@1234ABCD:test") - self.bob = hs.parse_userid("@4567:test") - self.alice = hs.parse_userid("@alice:remote") + self.frank = UserID.from_string("@1234ABCD:test") + self.bob = UserID.from_string("@4567:test") + self.alice = UserID.from_string("@alice:remote") yield self.store.create_profile(self.frank.localpart) diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py index d3253b48b8..9a23b3812d 100644 --- a/tests/handlers/test_room.py +++ b/tests/handlers/test_room.py @@ -15,12 +15,13 @@ from twisted.internet import defer -from tests import unittest +from .. import unittest from synapse.api.constants import EventTypes, Membership from synapse.handlers.room import RoomMemberHandler, RoomCreationHandler from synapse.handlers.profile import ProfileHandler from synapse.server import HomeServer +from synapse.types import UserID from ..utils import MockKey from mock import Mock, NonCallableMock @@ -164,7 +165,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase): event, context=context, ) self.notifier.on_new_room_event.assert_called_once_with( - event, extra_users=[self.hs.parse_userid(target_user_id)] + event, extra_users=[UserID.from_string(target_user_id)] ) self.assertFalse(self.datastore.get_room.called) self.assertFalse(self.datastore.store_room.called) @@ -174,7 +175,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase): def test_simple_join(self): room_id = "!foo:red" user_id = "@bob:red" - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) join_signal_observer = Mock() self.distributor.observe("user_joined_room", join_signal_observer) @@ -252,7 +253,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase): def test_simple_leave(self): room_id = "!foo:red" user_id = "@bob:red" - user = self.hs.parse_userid(user_id) + user = UserID.from_string(user_id) builder = self.hs.get_event_builder_factory().new({ "type": EventTypes.Member, diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 6a498b23a4..8a7fc028d1 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -27,6 +27,7 @@ from synapse.server import HomeServer from synapse.handlers.typing import TypingNotificationHandler from synapse.storage.transactions import DestinationsTable +from synapse.types import UserID def _expect_edu(destination, edu_type, content, origin="test"): @@ -153,11 +154,11 @@ class TypingNotificationsTestCase(unittest.TestCase): self.auth.check_joined_room = check_joined_room # Some local users to test with - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") + self.u_apple = UserID.from_string("@apple:test") + self.u_banana = UserID.from_string("@banana:test") # Remote user - self.u_onion = hs.parse_userid("@onion:farm") + self.u_onion = UserID.from_string("@onion:farm") @defer.inlineCallbacks def test_started_typing_local(self): diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index 783720ac29..65d5cc4916 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -27,6 +27,7 @@ from synapse.handlers.presence import PresenceHandler from synapse.server import HomeServer from synapse.rest.client.v1 import presence from synapse.rest.client.v1 import events +from synapse.types import UserID OFFLINE = PresenceState.OFFLINE @@ -71,7 +72,7 @@ class PresenceStateTestCase(unittest.TestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(myid), + "user": UserID.from_string(myid), "admin": False, "device_id": None, } @@ -90,7 +91,7 @@ class PresenceStateTestCase(unittest.TestCase): presence.register_servlets(hs, self.mock_resource) - self.u_apple = hs.parse_userid(myid) + self.u_apple = UserID.from_string(myid) @defer.inlineCallbacks def test_get_my_status(self): @@ -161,12 +162,12 @@ class PresenceListTestCase(unittest.TestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(myid), + "user": UserID.from_string(myid), "admin": False, "device_id": None, } - room_member_handler = hs.handlers.room_member_handler = Mock( + hs.handlers.room_member_handler = Mock( spec=[ "get_rooms_for_user", ] @@ -176,8 +177,8 @@ class PresenceListTestCase(unittest.TestCase): presence.register_servlets(hs, self.mock_resource) - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") + self.u_apple = UserID.from_string("@apple:test") + self.u_banana = UserID.from_string("@banana:test") @defer.inlineCallbacks def test_get_my_list(self): @@ -281,7 +282,7 @@ class PresenceEventStreamTestCase(unittest.TestCase): hs.get_clock().time_msec.return_value = 1000000 def _get_user_by_req(req=None): - return hs.parse_userid(myid) + return UserID.from_string(myid) hs.get_auth().get_user_by_req = _get_user_by_req @@ -322,8 +323,8 @@ class PresenceEventStreamTestCase(unittest.TestCase): self.presence = hs.get_handlers().presence_handler - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") + self.u_apple = UserID.from_string("@apple:test") + self.u_banana = UserID.from_string("@banana:test") @defer.inlineCallbacks def test_shortpoll(self): diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py index 5b5c3edc22..39cd68d829 100644 --- a/tests/rest/client/v1/test_profile.py +++ b/tests/rest/client/v1/test_profile.py @@ -24,6 +24,7 @@ from ....utils import MockHttpResource, MockKey from synapse.api.errors import SynapseError, AuthError from synapse.server import HomeServer +from synapse.types import UserID from synapse.rest.client.v1 import profile @@ -57,7 +58,7 @@ class ProfileTestCase(unittest.TestCase): ) def _get_user_by_req(request=None): - return hs.parse_userid(myid) + return UserID.from_string(myid) hs.get_auth().get_user_by_req = _get_user_by_req diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 12f8040541..76ed550b75 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -22,13 +22,10 @@ import synapse.rest.client.v1.room from synapse.api.constants import Membership from synapse.server import HomeServer +from synapse.types import UserID -from tests import unittest - -# python imports import json import urllib -import types from ....utils import MockHttpResource, SQLiteMemoryDbPool, MockKey from .utils import RestTestCase @@ -70,7 +67,7 @@ class RoomPermissionsTestCase(RestTestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(self.auth_user_id), + "user": UserID.from_string(self.auth_user_id), "admin": False, "device_id": None, } @@ -466,7 +463,7 @@ class RoomsMemberListTestCase(RestTestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(self.auth_user_id), + "user": UserID.from_string(self.auth_user_id), "admin": False, "device_id": None, } @@ -555,7 +552,7 @@ class RoomsCreateTestCase(RestTestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(self.auth_user_id), + "user": UserID.from_string(self.auth_user_id), "admin": False, "device_id": None, } @@ -657,7 +654,7 @@ class RoomTopicTestCase(RestTestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(self.auth_user_id), + "user": UserID.from_string(self.auth_user_id), "admin": False, "device_id": None, } @@ -773,7 +770,7 @@ class RoomMemberStateTestCase(RestTestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(self.auth_user_id), + "user": UserID.from_string(self.auth_user_id), "admin": False, "device_id": None, } @@ -909,7 +906,7 @@ class RoomMessagesTestCase(RestTestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(self.auth_user_id), + "user": UserID.from_string(self.auth_user_id), "admin": False, "device_id": None, } @@ -1013,7 +1010,7 @@ class RoomInitialSyncTestCase(RestTestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(self.auth_user_id), + "user": UserID.from_string(self.auth_user_id), "admin": False, "device_id": None, } @@ -1028,7 +1025,7 @@ class RoomInitialSyncTestCase(RestTestCase): # Since I'm getting my own presence I need to exist as far as presence # is concerned. hs.get_handlers().presence_handler.registered_user( - hs.parse_userid(self.user_id) + UserID.from_string(self.user_id) ) # create the room diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py index 647bcebfd8..c89b37d004 100644 --- a/tests/rest/client/v1/test_typing.py +++ b/tests/rest/client/v1/test_typing.py @@ -20,6 +20,7 @@ from twisted.internet import defer import synapse.rest.client.v1.room from synapse.server import HomeServer +from synapse.types import UserID from ....utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey from .utils import RestTestCase @@ -69,7 +70,7 @@ class RoomTypingTestCase(RestTestCase): def _get_user_by_token(token=None): return { - "user": hs.parse_userid(self.auth_user_id), + "user": UserID.from_string(self.auth_user_id), "admin": False, "device_id": None, } @@ -82,7 +83,7 @@ class RoomTypingTestCase(RestTestCase): def get_room_members(room_id): if room_id == self.room_id: - return defer.succeed([hs.parse_userid(self.user_id)]) + return defer.succeed([UserID.from_string(self.user_id)]) else: return defer.succeed([]) diff --git a/tests/storage/test_presence.py b/tests/storage/test_presence.py index 9655d3cf42..1ab193736b 100644 --- a/tests/storage/test_presence.py +++ b/tests/storage/test_presence.py @@ -19,6 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.storage.presence import PresenceStore +from synapse.types import UserID from tests.utils import SQLiteMemoryDbPool, MockClock @@ -37,8 +38,8 @@ class PresenceStoreTestCase(unittest.TestCase): self.store = PresenceStore(hs) - self.u_apple = hs.parse_userid("@apple:test") - self.u_banana = hs.parse_userid("@banana:test") + self.u_apple = UserID.from_string("@apple:test") + self.u_banana = UserID.from_string("@banana:test") @defer.inlineCallbacks def test_state(self): diff --git a/tests/storage/test_profile.py b/tests/storage/test_profile.py index 5d36723c28..84381241bc 100644 --- a/tests/storage/test_profile.py +++ b/tests/storage/test_profile.py @@ -19,6 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.storage.profile import ProfileStore +from synapse.types import UserID from tests.utils import SQLiteMemoryDbPool @@ -36,7 +37,7 @@ class ProfileStoreTestCase(unittest.TestCase): self.store = ProfileStore(hs) - self.u_frank = hs.parse_userid("@frank:test") + self.u_frank = UserID.from_string("@frank:test") @defer.inlineCallbacks def test_displayname(self): diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index 9806fbc69b..a16ccad881 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -19,6 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes, Membership +from synapse.types import UserID from tests.utils import SQLiteMemoryDbPool, MockKey @@ -48,8 +49,8 @@ class RedactionTestCase(unittest.TestCase): self.handlers = hs.get_handlers() self.message_handler = self.handlers.message_handler - self.u_alice = hs.parse_userid("@alice:test") - self.u_bob = hs.parse_userid("@bob:test") + self.u_alice = UserID.from_string("@alice:test") + self.u_bob = UserID.from_string("@bob:test") self.room1 = hs.parse_roomid("!abc123:test") diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py index e7739776ec..c6bfde069a 100644 --- a/tests/storage/test_room.py +++ b/tests/storage/test_room.py @@ -19,6 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes +from synapse.types import UserID from tests.utils import SQLiteMemoryDbPool @@ -40,7 +41,7 @@ class RoomStoreTestCase(unittest.TestCase): self.room = hs.parse_roomid("!abcde:test") self.alias = hs.parse_roomalias("#a-room-name:test") - self.u_creator = hs.parse_userid("@creator:test") + self.u_creator = UserID.from_string("@creator:test") yield self.store.store_room(self.room.to_string(), room_creator_user_id=self.u_creator.to_string(), diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index a23a8189df..6b7930b1d8 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -19,6 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes, Membership +from synapse.types import UserID from tests.utils import SQLiteMemoryDbPool, MockKey @@ -49,11 +50,11 @@ class RoomMemberStoreTestCase(unittest.TestCase): self.handlers = hs.get_handlers() self.message_handler = self.handlers.message_handler - self.u_alice = hs.parse_userid("@alice:test") - self.u_bob = hs.parse_userid("@bob:test") + self.u_alice = UserID.from_string("@alice:test") + self.u_bob = UserID.from_string("@bob:test") # User elsewhere on another host - self.u_charlie = hs.parse_userid("@charlie:elsewhere") + self.u_charlie = UserID.from_string("@charlie:elsewhere") self.room = hs.parse_roomid("!abc123:test") diff --git a/tests/storage/test_stream.py b/tests/storage/test_stream.py index 9247fc579e..d7c7f64d5e 100644 --- a/tests/storage/test_stream.py +++ b/tests/storage/test_stream.py @@ -19,6 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes, Membership +from synapse.types import UserID from tests.utils import SQLiteMemoryDbPool, MockKey @@ -48,8 +49,8 @@ class StreamStoreTestCase(unittest.TestCase): self.handlers = hs.get_handlers() self.message_handler = self.handlers.message_handler - self.u_alice = hs.parse_userid("@alice:test") - self.u_bob = hs.parse_userid("@bob:test") + self.u_alice = UserID.from_string("@alice:test") + self.u_bob = UserID.from_string("@bob:test") self.room1 = hs.parse_roomid("!abc123:test") self.room2 = hs.parse_roomid("!xyx987:test") diff --git a/tests/test_types.py b/tests/test_types.py index bfb9e6f548..2de7f22ab0 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -42,12 +42,6 @@ class UserIDTestCase(unittest.TestCase): self.assertTrue(userA == userAagain) self.assertTrue(userA != userB) - def test_via_homeserver(self): - user = mock_homeserver.parse_userid("@3456ijkl:my.domain") - - self.assertEquals("3456ijkl", user.localpart) - self.assertEquals("my.domain", user.domain) - class RoomAliasTestCase(unittest.TestCase): -- cgit 1.5.1 From 1c06c48ce2db3c6355e29de1533aebf36bc3775b Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 23 Jan 2015 11:55:12 +0000 Subject: Replace hs.parse_roomid with RoomID.from_string --- synapse/handlers/room.py | 2 -- synapse/rest/client/v1/room.py | 4 ++-- synapse/server.py | 7 +------ tests/storage/test_directory.py | 3 ++- tests/storage/test_redaction.py | 4 ++-- tests/storage/test_room.py | 6 +++--- tests/storage/test_roommember.py | 4 ++-- tests/storage/test_stream.py | 6 +++--- 8 files changed, 15 insertions(+), 21 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 0242288c4e..edb96cec83 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -246,8 +246,6 @@ class RoomMemberHandler(BaseHandler): @defer.inlineCallbacks def get_room_members(self, room_id): - hs = self.hs - users = yield self.store.get_users_in_room(room_id) defer.returnValue([UserID.from_string(u) for u in users]) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index c5837b3403..f0a9c932c1 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -20,7 +20,7 @@ from base import RestServlet, client_path_pattern from synapse.api.errors import SynapseError, Codes from synapse.streams.config import PaginationConfig from synapse.api.constants import EventTypes, Membership -from synapse.types import UserID +from synapse.types import UserID, RoomID import json import logging @@ -227,7 +227,7 @@ class JoinRoomAliasServlet(RestServlet): identifier = self.hs.parse_roomalias(room_identifier) is_room_alias = True except SynapseError: - identifier = self.hs.parse_roomid(room_identifier) + identifier = RoomID.from_string(room_identifier) # TODO: Support for specifying the home server to join with? diff --git a/synapse/server.py b/synapse/server.py index 52a21aaf78..4dfff04277 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -26,7 +26,7 @@ from synapse.api.auth import Auth from synapse.handlers import Handlers from synapse.state import StateHandler from synapse.storage import DataStore -from synapse.types import UserID, RoomAlias, RoomID, EventID +from synapse.types import RoomAlias, EventID from synapse.util import Clock from synapse.util.distributor import Distributor from synapse.util.lockutils import LockManager @@ -132,11 +132,6 @@ class BaseHomeServer(object): object.""" return RoomAlias.from_string(s) - 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) - def parse_eventid(self, s): """Parse the string given by 's' as a Event ID and return a EventID object.""" diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py index e9c242cc07..1bc6391766 100644 --- a/tests/storage/test_directory.py +++ b/tests/storage/test_directory.py @@ -19,6 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.storage.directory import DirectoryStore +from synapse.types import RoomID from tests.utils import SQLiteMemoryDbPool @@ -37,7 +38,7 @@ class DirectoryStoreTestCase(unittest.TestCase): self.store = DirectoryStore(hs) - self.room = hs.parse_roomid("!abcde:test") + self.room = RoomID.from_string("!abcde:test") self.alias = hs.parse_roomalias("#my-room:test") @defer.inlineCallbacks diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index a16ccad881..0713dfab64 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -19,7 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes, Membership -from synapse.types import UserID +from synapse.types import UserID, RoomID from tests.utils import SQLiteMemoryDbPool, MockKey @@ -52,7 +52,7 @@ class RedactionTestCase(unittest.TestCase): self.u_alice = UserID.from_string("@alice:test") self.u_bob = UserID.from_string("@bob:test") - self.room1 = hs.parse_roomid("!abc123:test") + self.room1 = RoomID.from_string("!abc123:test") self.depth = 1 diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py index c6bfde069a..baec3a3bb9 100644 --- a/tests/storage/test_room.py +++ b/tests/storage/test_room.py @@ -19,7 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes -from synapse.types import UserID +from synapse.types import UserID, RoomID from tests.utils import SQLiteMemoryDbPool @@ -39,7 +39,7 @@ class RoomStoreTestCase(unittest.TestCase): # management of the 'room_aliases' table self.store = hs.get_datastore() - self.room = hs.parse_roomid("!abcde:test") + self.room = RoomID.from_string("!abcde:test") self.alias = hs.parse_roomalias("#a-room-name:test") self.u_creator = UserID.from_string("@creator:test") @@ -98,7 +98,7 @@ class RoomEventsStoreTestCase(unittest.TestCase): self.store = hs.get_datastore() self.event_factory = hs.get_event_factory(); - self.room = hs.parse_roomid("!abcde:test") + self.room = RoomID.from_string("!abcde:test") yield self.store.store_room(self.room.to_string(), room_creator_user_id="@creator:text", diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 6b7930b1d8..2b9048e2a9 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -19,7 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes, Membership -from synapse.types import UserID +from synapse.types import UserID, RoomID from tests.utils import SQLiteMemoryDbPool, MockKey @@ -56,7 +56,7 @@ class RoomMemberStoreTestCase(unittest.TestCase): # User elsewhere on another host self.u_charlie = UserID.from_string("@charlie:elsewhere") - self.room = hs.parse_roomid("!abc123:test") + self.room = RoomID.from_string("!abc123:test") @defer.inlineCallbacks def inject_room_member(self, room, user, membership, replaces_state=None): diff --git a/tests/storage/test_stream.py b/tests/storage/test_stream.py index d7c7f64d5e..b7f6e2aa80 100644 --- a/tests/storage/test_stream.py +++ b/tests/storage/test_stream.py @@ -19,7 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes, Membership -from synapse.types import UserID +from synapse.types import UserID, RoomID from tests.utils import SQLiteMemoryDbPool, MockKey @@ -52,8 +52,8 @@ class StreamStoreTestCase(unittest.TestCase): self.u_alice = UserID.from_string("@alice:test") self.u_bob = UserID.from_string("@bob:test") - self.room1 = hs.parse_roomid("!abc123:test") - self.room2 = hs.parse_roomid("!xyx987:test") + self.room1 = RoomID.from_string("!abc123:test") + self.room2 = RoomID.from_string("!xyx987:test") self.depth = 1 -- cgit 1.5.1 From ada711504efb4dd25fe1123d38a0b2d196b9890a Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 23 Jan 2015 13:21:58 +0000 Subject: Replace hs.parse_roomalias with RoomAlias.from_string --- synapse/handlers/directory.py | 3 ++- synapse/rest/client/v1/directory.py | 9 +++++---- synapse/rest/client/v1/room.py | 4 ++-- synapse/server.py | 7 +------ tests/handlers/test_directory.py | 7 ++++--- tests/storage/test_directory.py | 4 ++-- tests/storage/test_room.py | 4 ++-- tests/test_types.py | 6 ------ 8 files changed, 18 insertions(+), 26 deletions(-) diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 91fceda2ac..58e9a91562 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -19,6 +19,7 @@ from ._base import BaseHandler from synapse.api.errors import SynapseError, Codes, CodeMessageException from synapse.api.constants import EventTypes +from synapse.types import RoomAlias import logging @@ -122,7 +123,7 @@ class DirectoryHandler(BaseHandler): @defer.inlineCallbacks def on_directory_query(self, args): - room_alias = self.hs.parse_roomalias(args["room_alias"]) + room_alias = RoomAlias.from_string(args["room_alias"]) if not self.hs.is_mine(room_alias): raise SynapseError( 400, "Room Alias is not hosted on this Home Server" diff --git a/synapse/rest/client/v1/directory.py b/synapse/rest/client/v1/directory.py index 7ff44fdd9e..1f33ec9e81 100644 --- a/synapse/rest/client/v1/directory.py +++ b/synapse/rest/client/v1/directory.py @@ -17,7 +17,8 @@ from twisted.internet import defer from synapse.api.errors import AuthError, SynapseError, Codes -from base import RestServlet, client_path_pattern +from synapse.types import RoomAlias +from .base import RestServlet, client_path_pattern import json import logging @@ -35,7 +36,7 @@ class ClientDirectoryServer(RestServlet): @defer.inlineCallbacks def on_GET(self, request, room_alias): - room_alias = self.hs.parse_roomalias(room_alias) + room_alias = RoomAlias.from_string(room_alias) dir_handler = self.handlers.directory_handler res = yield dir_handler.get_association(room_alias) @@ -53,7 +54,7 @@ class ClientDirectoryServer(RestServlet): logger.debug("Got content: %s", content) - room_alias = self.hs.parse_roomalias(room_alias) + room_alias = RoomAlias.from_string(room_alias) logger.debug("Got room name: %s", room_alias.to_string()) @@ -92,7 +93,7 @@ class ClientDirectoryServer(RestServlet): dir_handler = self.handlers.directory_handler - room_alias = self.hs.parse_roomalias(room_alias) + room_alias = RoomAlias.from_string(room_alias) yield dir_handler.delete_association( user.to_string(), room_alias diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index f0a9c932c1..42712d4a7c 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -20,7 +20,7 @@ from base import RestServlet, client_path_pattern from synapse.api.errors import SynapseError, Codes from synapse.streams.config import PaginationConfig from synapse.api.constants import EventTypes, Membership -from synapse.types import UserID, RoomID +from synapse.types import UserID, RoomID, RoomAlias import json import logging @@ -224,7 +224,7 @@ class JoinRoomAliasServlet(RestServlet): identifier = None is_room_alias = False try: - identifier = self.hs.parse_roomalias(room_identifier) + identifier = RoomAlias.from_string(room_identifier) is_room_alias = True except SynapseError: identifier = RoomID.from_string(room_identifier) diff --git a/synapse/server.py b/synapse/server.py index 4dfff04277..41a26ad91a 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -26,7 +26,7 @@ from synapse.api.auth import Auth from synapse.handlers import Handlers from synapse.state import StateHandler from synapse.storage import DataStore -from synapse.types import RoomAlias, EventID +from synapse.types import EventID from synapse.util import Clock from synapse.util.distributor import Distributor from synapse.util.lockutils import LockManager @@ -127,11 +127,6 @@ class BaseHomeServer(object): # 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? - 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) - def parse_eventid(self, s): """Parse the string given by 's' as a Event ID and return a EventID object.""" diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 8e164e4be0..22119de46a 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -21,6 +21,7 @@ from mock import Mock from synapse.server import HomeServer from synapse.handlers.directory import DirectoryHandler +from synapse.types import RoomAlias from tests.utils import SQLiteMemoryDbPool, MockKey @@ -65,9 +66,9 @@ class DirectoryTestCase(unittest.TestCase): self.store = hs.get_datastore() - self.my_room = hs.parse_roomalias("#my-room:test") - self.your_room = hs.parse_roomalias("#your-room:test") - self.remote_room = hs.parse_roomalias("#another:remote") + self.my_room = RoomAlias.from_string("#my-room:test") + self.your_room = RoomAlias.from_string("#your-room:test") + self.remote_room = RoomAlias.from_string("#another:remote") @defer.inlineCallbacks def test_get_local_association(self): diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py index 1bc6391766..bc9ebf35e2 100644 --- a/tests/storage/test_directory.py +++ b/tests/storage/test_directory.py @@ -19,7 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.storage.directory import DirectoryStore -from synapse.types import RoomID +from synapse.types import RoomID, RoomAlias from tests.utils import SQLiteMemoryDbPool @@ -39,7 +39,7 @@ class DirectoryStoreTestCase(unittest.TestCase): self.store = DirectoryStore(hs) self.room = RoomID.from_string("!abcde:test") - self.alias = hs.parse_roomalias("#my-room:test") + self.alias = RoomAlias.from_string("#my-room:test") @defer.inlineCallbacks def test_room_to_alias(self): diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py index baec3a3bb9..71e5d34143 100644 --- a/tests/storage/test_room.py +++ b/tests/storage/test_room.py @@ -19,7 +19,7 @@ from twisted.internet import defer from synapse.server import HomeServer from synapse.api.constants import EventTypes -from synapse.types import UserID, RoomID +from synapse.types import UserID, RoomID, RoomAlias from tests.utils import SQLiteMemoryDbPool @@ -40,7 +40,7 @@ class RoomStoreTestCase(unittest.TestCase): self.store = hs.get_datastore() self.room = RoomID.from_string("!abcde:test") - self.alias = hs.parse_roomalias("#a-room-name:test") + self.alias = RoomAlias.from_string("#a-room-name:test") self.u_creator = UserID.from_string("@creator:test") yield self.store.store_room(self.room.to_string(), diff --git a/tests/test_types.py b/tests/test_types.py index 2de7f22ab0..b29a8415b1 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -56,9 +56,3 @@ class RoomAliasTestCase(unittest.TestCase): room = RoomAlias("channel", "my.domain") self.assertEquals(room.to_string(), "#channel:my.domain") - - def test_via_homeserver(self): - room = mock_homeserver.parse_roomalias("#elsewhere:my.domain") - - self.assertEquals("elsewhere", room.localpart) - self.assertEquals("my.domain", room.domain) -- cgit 1.5.1 From c4652d7772a0d9b374fc178502a71efd03d35d48 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 23 Jan 2015 13:25:07 +0000 Subject: Remove hs.parse_eventid --- synapse/server.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/synapse/server.py b/synapse/server.py index 41a26ad91a..32013b1a91 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -26,7 +26,6 @@ from synapse.api.auth import Auth from synapse.handlers import Handlers from synapse.state import StateHandler from synapse.storage import DataStore -from synapse.types import EventID from synapse.util import Clock from synapse.util.distributor import Distributor from synapse.util.lockutils import LockManager @@ -124,14 +123,6 @@ 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? - - def parse_eventid(self, s): - """Parse the string given by 's' as a Event ID and return a EventID - object.""" - return EventID.from_string(s) - def serialize_event(self, e, as_client_event=True): return serialize_event(self, e, as_client_event) -- cgit 1.5.1 From 4be637cb120f00e5de99c12a79e908e2f26da8af Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 23 Jan 2015 14:09:51 +0000 Subject: Extract the client v1 base RestServlet to a separate class --- synapse/rest/client/v1/admin.py | 4 ++-- synapse/rest/client/v1/base.py | 40 +++++----------------------------- synapse/rest/client/v1/directory.py | 4 ++-- synapse/rest/client/v1/events.py | 6 ++--- synapse/rest/client/v1/initial_sync.py | 4 ++-- synapse/rest/client/v1/login.py | 8 +++---- synapse/rest/client/v1/presence.py | 6 ++--- synapse/rest/client/v1/profile.py | 8 +++---- synapse/rest/client/v1/register.py | 4 ++-- synapse/rest/client/v1/room.py | 28 ++++++++++++------------ synapse/rest/client/v1/voip.py | 4 ++-- 11 files changed, 44 insertions(+), 72 deletions(-) diff --git a/synapse/rest/client/v1/admin.py b/synapse/rest/client/v1/admin.py index 4aefb94053..1051d96f96 100644 --- a/synapse/rest/client/v1/admin.py +++ b/synapse/rest/client/v1/admin.py @@ -18,14 +18,14 @@ from twisted.internet import defer from synapse.api.errors import AuthError, SynapseError from synapse.types import UserID -from base import RestServlet, client_path_pattern +from base import ClientV1RestServlet, client_path_pattern import logging logger = logging.getLogger(__name__) -class WhoisRestServlet(RestServlet): +class WhoisRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/admin/whois/(?P[^/]*)") @defer.inlineCallbacks diff --git a/synapse/rest/client/v1/base.py b/synapse/rest/client/v1/base.py index d005206b77..72332bdb10 100644 --- a/synapse/rest/client/v1/base.py +++ b/synapse/rest/client/v1/base.py @@ -13,7 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" This module contains base REST classes for constructing REST servlets. """ +"""This module contains base REST classes for constructing client v1 servlets. +""" + +from synapse.http.servlet import RestServlet from synapse.api.urls import CLIENT_PREFIX from .transactions import HttpTransactionStore import re @@ -37,44 +40,13 @@ def client_path_pattern(path_regex): return re.compile("^" + CLIENT_PREFIX + path_regex) -class RestServlet(object): - - """ A Synapse REST Servlet. - - An implementing class can either provide its own custom 'register' method, - or use the automatic pattern handling provided by the base class. - - To use this latter, the implementing class instead provides a `PATTERN` - class attribute containing a pre-compiled regular expression. The automatic - register method will then use this method to register any of the following - instance methods associated with the corresponding HTTP method: - - on_GET - on_PUT - on_POST - on_DELETE - on_OPTIONS - - Automatically handles turning CodeMessageExceptions thrown by these methods - into the appropriate HTTP response. +class ClientV1RestServlet(RestServlet): + """A base Synapse REST Servlet for the client version 1 API. """ def __init__(self, hs): self.hs = hs - self.handlers = hs.get_handlers() self.builder_factory = hs.get_event_builder_factory() self.auth = hs.get_auth() self.txns = HttpTransactionStore() - - def register(self, http_server): - """ Register this servlet with the given HTTP server. """ - if hasattr(self, "PATTERN"): - pattern = self.PATTERN - - for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"): - if hasattr(self, "on_%s" % (method)): - method_handler = getattr(self, "on_%s" % (method)) - http_server.register_path(method, pattern, method_handler) - else: - raise NotImplementedError("RestServlet must register something.") diff --git a/synapse/rest/client/v1/directory.py b/synapse/rest/client/v1/directory.py index 1f33ec9e81..15ae8749b8 100644 --- a/synapse/rest/client/v1/directory.py +++ b/synapse/rest/client/v1/directory.py @@ -18,7 +18,7 @@ from twisted.internet import defer from synapse.api.errors import AuthError, SynapseError, Codes from synapse.types import RoomAlias -from .base import RestServlet, client_path_pattern +from .base import ClientV1RestServlet, client_path_pattern import json import logging @@ -31,7 +31,7 @@ def register_servlets(hs, http_server): ClientDirectoryServer(hs).register(http_server) -class ClientDirectoryServer(RestServlet): +class ClientDirectoryServer(ClientV1RestServlet): PATTERN = client_path_pattern("/directory/room/(?P[^/]*)$") @defer.inlineCallbacks diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/v1/events.py index c2515528ac..c69de56863 100644 --- a/synapse/rest/client/v1/events.py +++ b/synapse/rest/client/v1/events.py @@ -18,7 +18,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError from synapse.streams.config import PaginationConfig -from .base import RestServlet, client_path_pattern +from .base import ClientV1RestServlet, client_path_pattern import logging @@ -26,7 +26,7 @@ import logging logger = logging.getLogger(__name__) -class EventStreamRestServlet(RestServlet): +class EventStreamRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/events$") DEFAULT_LONGPOLL_TIME_MS = 30000 @@ -61,7 +61,7 @@ class EventStreamRestServlet(RestServlet): # TODO: Unit test gets, with and without auth, with different kinds of events. -class EventRestServlet(RestServlet): +class EventRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/events/(?P[^/]*)$") @defer.inlineCallbacks diff --git a/synapse/rest/client/v1/initial_sync.py b/synapse/rest/client/v1/initial_sync.py index b13d56b286..357fa845b4 100644 --- a/synapse/rest/client/v1/initial_sync.py +++ b/synapse/rest/client/v1/initial_sync.py @@ -16,11 +16,11 @@ from twisted.internet import defer from synapse.streams.config import PaginationConfig -from base import RestServlet, client_path_pattern +from base import ClientV1RestServlet, client_path_pattern # TODO: Needs unit testing -class InitialSyncRestServlet(RestServlet): +class InitialSyncRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/initialSync$") @defer.inlineCallbacks diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 6b8deff67b..7116ac98e8 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -17,12 +17,12 @@ from twisted.internet import defer from synapse.api.errors import SynapseError from synapse.types import UserID -from base import RestServlet, client_path_pattern +from base import ClientV1RestServlet, client_path_pattern import json -class LoginRestServlet(RestServlet): +class LoginRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/login$") PASS_TYPE = "m.login.password" @@ -64,7 +64,7 @@ class LoginRestServlet(RestServlet): defer.returnValue((200, result)) -class LoginFallbackRestServlet(RestServlet): +class LoginFallbackRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/login/fallback$") def on_GET(self, request): @@ -73,7 +73,7 @@ class LoginFallbackRestServlet(RestServlet): return (200, {}) -class PasswordResetRestServlet(RestServlet): +class PasswordResetRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/login/reset") @defer.inlineCallbacks diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/v1/presence.py index 22fcb7d7d0..b6c207e662 100644 --- a/synapse/rest/client/v1/presence.py +++ b/synapse/rest/client/v1/presence.py @@ -19,7 +19,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError from synapse.types import UserID -from .base import RestServlet, client_path_pattern +from .base import ClientV1RestServlet, client_path_pattern import json import logging @@ -27,7 +27,7 @@ import logging logger = logging.getLogger(__name__) -class PresenceStatusRestServlet(RestServlet): +class PresenceStatusRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/presence/(?P[^/]*)/status") @defer.inlineCallbacks @@ -72,7 +72,7 @@ class PresenceStatusRestServlet(RestServlet): return (200, {}) -class PresenceListRestServlet(RestServlet): +class PresenceListRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/presence/list/(?P[^/]*)") @defer.inlineCallbacks diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py index 39297930c8..24f8d56952 100644 --- a/synapse/rest/client/v1/profile.py +++ b/synapse/rest/client/v1/profile.py @@ -16,13 +16,13 @@ """ This module contains REST servlets to do with profile: /profile/ """ from twisted.internet import defer -from .base import RestServlet, client_path_pattern +from .base import ClientV1RestServlet, client_path_pattern from synapse.types import UserID import json -class ProfileDisplaynameRestServlet(RestServlet): +class ProfileDisplaynameRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/profile/(?P[^/]*)/displayname") @defer.inlineCallbacks @@ -55,7 +55,7 @@ class ProfileDisplaynameRestServlet(RestServlet): return (200, {}) -class ProfileAvatarURLRestServlet(RestServlet): +class ProfileAvatarURLRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/profile/(?P[^/]*)/avatar_url") @defer.inlineCallbacks @@ -88,7 +88,7 @@ class ProfileAvatarURLRestServlet(RestServlet): return (200, {}) -class ProfileRestServlet(RestServlet): +class ProfileRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/profile/(?P[^/]*)") @defer.inlineCallbacks diff --git a/synapse/rest/client/v1/register.py b/synapse/rest/client/v1/register.py index e3b26902d9..c0423c2d45 100644 --- a/synapse/rest/client/v1/register.py +++ b/synapse/rest/client/v1/register.py @@ -18,7 +18,7 @@ from twisted.internet import defer from synapse.api.errors import SynapseError, Codes from synapse.api.constants import LoginType -from base import RestServlet, client_path_pattern +from base import ClientV1RestServlet, client_path_pattern import synapse.util.stringutils as stringutils from synapse.util.async import run_on_reactor @@ -42,7 +42,7 @@ else: compare_digest = lambda a, b: a == b -class RegisterRestServlet(RestServlet): +class RegisterRestServlet(ClientV1RestServlet): """Handles registration with the home server. This servlet is in control of the registration flow; the registration diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 42712d4a7c..f06e3ddb98 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -16,7 +16,7 @@ """ This module contains REST servlets to do with rooms: /rooms/ """ from twisted.internet import defer -from base import RestServlet, client_path_pattern +from base import ClientV1RestServlet, client_path_pattern from synapse.api.errors import SynapseError, Codes from synapse.streams.config import PaginationConfig from synapse.api.constants import EventTypes, Membership @@ -30,7 +30,7 @@ import urllib logger = logging.getLogger(__name__) -class RoomCreateRestServlet(RestServlet): +class RoomCreateRestServlet(ClientV1RestServlet): # No PATTERN; we have custom dispatch rules here def register(self, http_server): @@ -94,7 +94,7 @@ class RoomCreateRestServlet(RestServlet): # TODO: Needs unit testing for generic events -class RoomStateEventRestServlet(RestServlet): +class RoomStateEventRestServlet(ClientV1RestServlet): def register(self, http_server): # /room/$roomid/state/$eventtype no_state_key = "/rooms/(?P[^/]*)/state/(?P[^/]*)$" @@ -163,7 +163,7 @@ class RoomStateEventRestServlet(RestServlet): # TODO: Needs unit testing for generic events + feedback -class RoomSendEventRestServlet(RestServlet): +class RoomSendEventRestServlet(ClientV1RestServlet): def register(self, http_server): # /rooms/$roomid/send/$event_type[/$txn_id] @@ -206,7 +206,7 @@ class RoomSendEventRestServlet(RestServlet): # TODO: Needs unit testing for room ID + alias joins -class JoinRoomAliasServlet(RestServlet): +class JoinRoomAliasServlet(ClientV1RestServlet): def register(self, http_server): # /join/$room_identifier[/$txn_id] @@ -265,7 +265,7 @@ class JoinRoomAliasServlet(RestServlet): # TODO: Needs unit testing -class PublicRoomListRestServlet(RestServlet): +class PublicRoomListRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/publicRooms$") @defer.inlineCallbacks @@ -276,7 +276,7 @@ class PublicRoomListRestServlet(RestServlet): # TODO: Needs unit testing -class RoomMemberListRestServlet(RestServlet): +class RoomMemberListRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/rooms/(?P[^/]*)/members$") @defer.inlineCallbacks @@ -305,7 +305,7 @@ class RoomMemberListRestServlet(RestServlet): # TODO: Needs unit testing -class RoomMessageListRestServlet(RestServlet): +class RoomMessageListRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/rooms/(?P[^/]*)/messages$") @defer.inlineCallbacks @@ -329,7 +329,7 @@ class RoomMessageListRestServlet(RestServlet): # TODO: Needs unit testing -class RoomStateRestServlet(RestServlet): +class RoomStateRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/rooms/(?P[^/]*)/state$") @defer.inlineCallbacks @@ -345,7 +345,7 @@ class RoomStateRestServlet(RestServlet): # TODO: Needs unit testing -class RoomInitialSyncRestServlet(RestServlet): +class RoomInitialSyncRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/rooms/(?P[^/]*)/initialSync$") @defer.inlineCallbacks @@ -360,7 +360,7 @@ class RoomInitialSyncRestServlet(RestServlet): defer.returnValue((200, content)) -class RoomTriggerBackfill(RestServlet): +class RoomTriggerBackfill(ClientV1RestServlet): PATTERN = client_path_pattern("/rooms/(?P[^/]*)/backfill$") @defer.inlineCallbacks @@ -379,7 +379,7 @@ class RoomTriggerBackfill(RestServlet): # TODO: Needs unit testing -class RoomMembershipRestServlet(RestServlet): +class RoomMembershipRestServlet(ClientV1RestServlet): def register(self, http_server): # /rooms/$roomid/[invite|join|leave] @@ -431,7 +431,7 @@ class RoomMembershipRestServlet(RestServlet): defer.returnValue(response) -class RoomRedactEventRestServlet(RestServlet): +class RoomRedactEventRestServlet(ClientV1RestServlet): def register(self, http_server): PATTERN = ("/rooms/(?P[^/]*)/redact/(?P[^/]*)") register_txn_path(self, PATTERN, http_server) @@ -469,7 +469,7 @@ class RoomRedactEventRestServlet(RestServlet): defer.returnValue(response) -class RoomTypingRestServlet(RestServlet): +class RoomTypingRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern( "/rooms/(?P[^/]*)/typing/(?P[^/]*)$" ) diff --git a/synapse/rest/client/v1/voip.py b/synapse/rest/client/v1/voip.py index 011c35e69b..822d863ce6 100644 --- a/synapse/rest/client/v1/voip.py +++ b/synapse/rest/client/v1/voip.py @@ -15,7 +15,7 @@ from twisted.internet import defer -from base import RestServlet, client_path_pattern +from base import ClientV1RestServlet, client_path_pattern import hmac @@ -23,7 +23,7 @@ import hashlib import base64 -class VoipRestServlet(RestServlet): +class VoipRestServlet(ClientV1RestServlet): PATTERN = client_path_pattern("/voip/turnServer$") @defer.inlineCallbacks -- cgit 1.5.1 From e0bf18addf643bd72a0cd5cb8dbc127dc7a7e8d2 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 23 Jan 2015 14:16:28 +0000 Subject: Add RestServlet base class in synapse/http/servlet.py --- synapse/http/servlet.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 synapse/http/servlet.py diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py new file mode 100644 index 0000000000..d5ccf2742f --- /dev/null +++ b/synapse/http/servlet.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 2015 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains base REST classes for constructing REST servlets. """ + +import logging + + +logger = logging.getLogger(__name__) + + +class RestServlet(object): + + """ A Synapse REST Servlet. + + An implementing class can either provide its own custom 'register' method, + or use the automatic pattern handling provided by the base class. + + To use this latter, the implementing class instead provides a `PATTERN` + class attribute containing a pre-compiled regular expression. The automatic + register method will then use this method to register any of the following + instance methods associated with the corresponding HTTP method: + + on_GET + on_PUT + on_POST + on_DELETE + on_OPTIONS + + Automatically handles turning CodeMessageExceptions thrown by these methods + into the appropriate HTTP response. + """ + + def register(self, http_server): + """ Register this servlet with the given HTTP server. """ + if hasattr(self, "PATTERN"): + pattern = self.PATTERN + + for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"): + if hasattr(self, "on_%s" % (method)): + method_handler = getattr(self, "on_%s" % (method)) + http_server.register_path(method, pattern, method_handler) + else: + raise NotImplementedError("RestServlet must register something.") -- cgit 1.5.1 From 30a89d2fdb8db9a06974fd9732c4eab5263dea79 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 23 Jan 2015 15:51:21 +0000 Subject: Update .gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 2ed22b1cd3..3899c5a250 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,9 @@ uploads .idea/ media_store/ + +*.tac + +build/ + +localhost-800*/ -- cgit 1.5.1