Compare commits

...

191 Commits

Author SHA1 Message Date
Erik Johnston
463b95f0c2 Bump version and change log 2014-08-29 15:36:53 +01:00
Emmanuel ROHEE
bf6466f02a The away state is unavailable not offline 2014-08-29 15:29:26 +02:00
David Baker
4b7f6dd7fc Only show voice call button if there are exactly 2 members in the room. Also hide the somewhat user unfriendly call state. 2014-08-29 14:00:20 +01:00
David Baker
073bec4830 Oops, forgot a s/sendObject/sendEvent/ - make messages work again! 2014-08-29 13:45:15 +01:00
David Baker
cc413be446 Don't break if the call ends before it connects 2014-08-29 13:28:39 +01:00
Erik Johnston
ee06023573 Get the equalities right. 2014-08-29 13:28:06 +01:00
David Baker
3e6a19cf09 Merge branch 'develop' of github.com:matrix-org/synapse into develop 2014-08-29 13:24:08 +01:00
David Baker
5308e3026a Change call signalling messages to be their own types of room events rather than room messages with different msgtypes: room messages should be things that the client can display as a unit message to the user. 2014-08-29 13:23:01 +01:00
Emmanuel ROHEE
eab463fda5 Show notifications only when the user is detected as idle 2014-08-29 13:44:10 +02:00
Erik Johnston
47fb286184 Merge branch 'presence_logging' into develop 2014-08-29 12:10:00 +01:00
Erik Johnston
5dd38d579b Fix a couple of bugs in presence handler related to pushing updatesto the correct user. Fix presence tests. 2014-08-29 12:08:33 +01:00
Kegan Dougal
ac56ac67cc Expand architecture section to introduce room IDs, room aliases, user IDs, events and federation. 2014-08-29 11:42:05 +01:00
David Baker
171d8b032f Merge branch 'voip' into develop
Conflicts:
	webclient/room/room-controller.js
2014-08-29 11:33:36 +01:00
David Baker
41d02ab674 More basic functionality for voip calls (like hanging up) 2014-08-29 11:29:36 +01:00
Emmanuel ROHEE
1abc93d65c Cleaned up ng deps. By convention, angular modules must be listed at first 2014-08-29 11:58:35 +02:00
Emmanuel ROHEE
ee079cd250 Added a timeout(40s) to $http stream requests (/events) in order to be notified by an error when there is a network issue. Thus, we can retry with a new request. 2014-08-29 11:32:06 +02:00
Kegan Dougal
d1bf659ed7 Redo architecture diagram. Reword parts of federation. Formatting fixes and tweaks. 2014-08-29 10:30:14 +01:00
Emmanuel ROHEE
089d1b1b78 Recents update: do not care of events coming from the past (they are fired when doing pagination of room messages in the past) 2014-08-29 09:55:47 +02:00
Emmanuel ROHEE
9b2cb41dcf Display emotes in the recents list 2014-08-29 09:49:03 +02:00
Emmanuel ROHEE
96baf62e7a ng-show exists. So, for clarity, avoid to use ng-hide and double negation test. 2014-08-29 09:32:09 +02:00
Emmanuel ROHEE
246b2a3c3e Renamed matrixService.assignRoomAliases into getRoomAliasAndDisplayName 2014-08-29 09:32:09 +02:00
David Baker
ca7426eee0 First basic working VoIP call support 2014-08-28 19:03:34 +01:00
Erik Johnston
8113eb7c79 Turn of trace_function logging 2014-08-28 18:45:00 +01:00
Erik Johnston
aaf4fd98ee Only poll remote users if they are in our presence list, rather than in a common room 2014-08-28 18:43:03 +01:00
Mark Haines
722c19d033 Fix FederationHandler to event.origin 2014-08-28 18:32:44 +01:00
Erik Johnston
1b7686329e Don't query the rooms members table so much by using the new notifier api that allows you to specify room_ids to notify. 2014-08-28 17:43:15 +01:00
Kegan Dougal
068b348e7e Start fleshing out architecture section. Moar .rst formatting! Reword some copypastaed sections to be terser. 2014-08-28 17:40:12 +01:00
Paul "LeoNerd" Evans
2c7c12bc6e Initial room event stream token must be s0, not s1, or everyone will miss the very first room event 2014-08-28 17:39:34 +01:00
Erik Johnston
54d0a75573 Merge branch 'develop' of github.com:matrix-org/synapse into presence_logging
Conflicts:
	synapse/handlers/presence.py
2014-08-28 16:52:46 +01:00
Erik Johnston
a8d318cf82 Up timeout to 10 minutes 2014-08-28 16:44:09 +01:00
Paul "LeoNerd" Evans
efc5f3440d Only send presence "poll"/"unpoll" EDUs when changing from/to zero remotes 2014-08-28 16:43:55 +01:00
Paul "LeoNerd" Evans
113342a756 Ability to assert a DeferredMockCallable has received no calls 2014-08-28 16:40:06 +01:00
Paul "LeoNerd" Evans
b1da3fa0a7 Avoid AlreadyCalledError from EDU sending failures 2014-08-28 16:19:16 +01:00
Paul "LeoNerd" Evans
c46c806126 Re-enable presence, un-skip presence tests 2014-08-28 16:00:14 +01:00
Erik Johnston
eb3094ed31 And more logging. 2014-08-28 15:58:38 +01:00
Emmanuel ROHEE
b09e531159 Do a smart update of the recents from the events stream rather than hammering initialSync each time 2014-08-28 16:38:16 +02:00
Kegan Dougal
62dfa3c741 Flesh out m.room.message msgtypes 2014-08-28 15:35:28 +01:00
Mark Haines
7b079a26a5 Remove get_state_for_room function from federation handler 2014-08-28 15:32:38 +01:00
Mark Haines
bddc1d9fff use @wraps to set the __name__ __module__ and __doc__ correctly for logged functions 2014-08-28 15:32:38 +01:00
Erik Johnston
e0ba81344c Add more logging. Up the event stream timer to 10s 2014-08-28 15:30:42 +01:00
Emmanuel ROHEE
c44293db2f When opening this page, do not join a room already joined 2014-08-28 16:23:30 +02:00
Emmanuel ROHEE
7c99ebdbd1 Added waitForInitialSyncCompletion so that clients can know when they can access to the data retrieved by the initialSync Request 2014-08-28 16:23:30 +02:00
Emmanuel ROHEE
06c79a23d4 BF: Made member events parsing work (handleEvents expects an array of events) 2014-08-28 16:23:30 +02:00
Emmanuel ROHEE
466fbe4c4e Cleaned up deps 2014-08-28 16:23:30 +02:00
Erik Johnston
b8b52ca09d Add logging to try and figure out what is going on with the presence stuff 2014-08-28 14:58:51 +01:00
Kegan Dougal
8d7d251c35 Support multiple login flows when deciding how to login. Updated cmdclient and spec. Webclient doesn't need updating for this. 2014-08-28 14:56:55 +01:00
Kegan Dougal
52cfdfd5f1 Fleshed out login spec. 2014-08-28 14:49:21 +01:00
Mark Haines
7acede1e42 Fix pyflakes warnings 2014-08-28 13:51:50 +01:00
Mark Haines
15ab5f5ad8 Merge backfill_ and backfill in federation handler 2014-08-28 13:45:35 +01:00
Erik Johnston
b485d622cc Fix bug where we used UserID objects instead of strigns 2014-08-28 13:40:27 +01:00
Kegan Dougal
64e927108b Added skeleton specification for a general feel of the layout. 2014-08-28 11:35:24 +01:00
Erik Johnston
f3f32addca Fix typo in NullSource.get_pagination_rows. Remove unused import. 2014-08-28 10:57:53 +01:00
Emmanuel ROHEE
6ac298f2f1 Start the events stream once the app starts (if credentials are in cache) or once the user gets logged in 2014-08-28 11:04:15 +02:00
Kegan Dougal
660129deb1 Shuffle files around in /docs 2014-08-28 09:45:05 +01:00
David Baker
7d34a1c108 WIP voip support on web client 2014-08-27 18:57:54 +01:00
Paul "LeoNerd" Evans
d027e859cd Fix up the various presence-related tests so that if they're not skipped, they still PASS 2014-08-27 18:30:09 +01:00
Paul "LeoNerd" Evans
407c86c013 Define a NullSource useful for unit-testing 2014-08-27 18:30:09 +01:00
Erik Johnston
c2b4b73751 Split out MessageHandler 2014-08-27 17:59:36 +01:00
Emmanuel ROHEE
04fdcf302d Wired the recents list with the stream events for realtime update 2014-08-27 18:52:15 +02:00
Mark Haines
357dd1871d Merge branch 'develop' into storage_transactions
Conflicts:
	tests/handlers/test_federation.py
	tests/handlers/test_room.py
2014-08-27 17:28:55 +01:00
Erik Johnston
e111a06e0a Fix tests. 2014-08-27 17:21:48 +01:00
Erik Johnston
410a74b0f3 If timeout=0, return immediately 2014-08-27 17:21:48 +01:00
Paul "LeoNerd" Evans
92033e4ebc Add python shebang line and chmod +x setup.py 2014-08-27 17:17:38 +01:00
Mark Haines
2aeaa7b77c Merge branch 'develop' into storage_transactions
Conflicts:
	synapse/handlers/room.py
	synapse/storage/stream.py
2014-08-27 17:15:58 +01:00
Erik Johnston
7c89d5e97a Merge branch 'develop' of github.com:matrix-org/synapse into develop 2014-08-27 17:05:48 +01:00
Erik Johnston
226025e9ca Comments! 2014-08-27 17:04:47 +01:00
Mark Haines
f54b70520a Return the store_id from persist_event 2014-08-27 17:03:45 +01:00
Matthew Hodgson
f53c4300fd improve iOS layout a bit 2014-08-27 17:03:16 +01:00
Kegan Dougal
6ad9d9c226 Added /rooms/$roomid/state and /rooms/$roomid/initialSync to API docs. 2014-08-27 17:02:08 +01:00
Emmanuel ROHEE
234c50b834 BF: mFileInput dependency got lost somewhere and upload buttons did not work anymore 2014-08-27 18:00:19 +02:00
Mark Haines
1d95e78759 Merge branch 'develop' into storage_transactions 2014-08-27 16:54:12 +01:00
Mark Haines
b30358f439 add _get_room_member, fix datastore methods 2014-08-27 16:51:54 +01:00
Kegan Dougal
f64887e15c Added RestServlet for /rooms/$roomid/initialSync 2014-08-27 16:49:01 +01:00
Erik Johnston
52cb5e6324 Remove stale FIXMEs 2014-08-27 16:44:29 +01:00
Kegan Dougal
4e8d19ee2b Added RestServlet for /rooms/$roomid/state 2014-08-27 16:42:33 +01:00
Erik Johnston
8af5e360d6 Remove store_id from notifier.on_new_room_event calls. 2014-08-27 16:23:33 +01:00
Emmanuel ROHEE
d9155b6a25 Highlight the current room in the recents list 2014-08-27 17:20:53 +02:00
Emmanuel ROHEE
7ee5288849 Added the recents component at the left hand side of the room page 2014-08-27 17:20:53 +02:00
Kegan Dougal
e179ed1f60 Added generic state/non-state event sending to the API docs. 2014-08-27 16:16:40 +01:00
Erik Johnston
89c044c2a0 Merge branch 'stream_refactor' into develop 2014-08-27 16:11:43 +01:00
Erik Johnston
7917ff1271 Turn off presence again. 2014-08-27 16:09:48 +01:00
Kegan Dougal
abe2035d85 api docs: Finished adding all C-S APIs. Added initialSync, publicRooms, membership changes (generic and RPCy) and directory paths. 2014-08-27 15:41:38 +01:00
Erik Johnston
08881d808d Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor 2014-08-27 15:34:01 +01:00
Erik Johnston
bfe9faad5a Index sources in a nicer fashion. 2014-08-27 15:33:52 +01:00
Erik Johnston
05672a6a8c Convert get_paginat_rows to use PaginationConfig. This allows people to supply directions. 2014-08-27 15:25:27 +01:00
Emmanuel ROHEE
fb9661898d BF: use room_id if there is no alias 2014-08-27 16:24:23 +02:00
Mark Haines
a0d1f5a014 Start updating state handling to use snapshots 2014-08-27 15:11:51 +01:00
Emmanuel ROHEE
87190a9673 Sort recents in anti-chronological order 2014-08-27 15:55:51 +02:00
Emmanuel ROHEE
308c9273fa Moved recents things into a separate (and reusable) controler 2014-08-27 15:55:51 +02:00
Emmanuel ROHEE
c67cac134f Moved assignRoomAliases into a central piece: matrixService for now 2014-08-27 15:55:51 +02:00
Emmanuel ROHEE
43242a0657 Cleaned ng dependencies 2014-08-27 15:55:51 +02:00
Emmanuel ROHEE
b1352f97ac home/recents: show the last message of each message 2014-08-27 15:55:51 +02:00
Emmanuel ROHEE
6691ca6f8d Rename go() into goToPage() which is available from everywhere thanks to the inheritance of $scope 2014-08-27 15:55:51 +02:00
Emmanuel ROHEE
e40d829363 Support limit and feedback param of initialSync 2014-08-27 15:55:51 +02:00
Kegan Dougal
c585c87c4b Renamed /ds to /directory 2014-08-27 14:54:29 +01:00
Kegan Dougal
1d9d287c7c Renamed /public/rooms to /publicRooms 2014-08-27 14:52:07 +01:00
Mark Haines
46a2f6a816 Remove call to get_federation from homeserver 2014-08-27 14:36:20 +01:00
Mark Haines
a03c7f27a8 Fill out prev_events before calling persist_event 2014-08-27 14:32:19 +01:00
Erik Johnston
77a255c7c3 PEP8 tweaks. 2014-08-27 14:19:39 +01:00
Erik Johnston
47519cd8c2 Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor
Conflicts:
	synapse/handlers/events.py
	synapse/rest/events.py
	synapse/rest/room.py
2014-08-27 14:13:06 +01:00
Erik Johnston
bd16b93e8f Implement presence event source. Change the way the notifier indexes listeners 2014-08-27 14:03:27 +01:00
David Baker
474d913712 fix joining rooms on webclient 2014-08-27 13:59:14 +01:00
Paul "LeoNerd" Evans
dddf5c0cc8 git ignore all the homeserver*.db files 2014-08-27 13:08:55 +01:00
Paul "LeoNerd" Evans
05fa81fee4 A reliable logger.info() message /after/ the TCP port has been opened and is listening; this is essential for avoiding races in wrapper scripts e.g. integration testing 2014-08-27 13:08:55 +01:00
Kegan Dougal
71095f4e6e Updated swagger JSON: cleaned up unused entries. Converted most paths to the new format. 2014-08-27 12:14:35 +01:00
Kegan Dougal
6c609425ba Removed urls.rst - The API docs / swagger JSON should be used as the canonical source for the REST API. Keeping urls.rst around is just an extra maintenance burden. 2014-08-27 12:14:35 +01:00
Paul "LeoNerd" Evans
5eff05a4ce Initial typing notification support - EDU federation, but no timers, and no actual push to clients 2014-08-27 11:45:16 +01:00
Paul "LeoNerd" Evans
d63f775e06 Added parse_roomid() helper 2014-08-27 11:45:16 +01:00
Paul "LeoNerd" Evans
e677a3114e Use SQLite's PRAGMA user_version to check if the database file really matches the schema we have in mind 2014-08-27 11:45:16 +01:00
Paul "LeoNerd" Evans
648796ef1d Neater database setup at application startup time; only .connect() it once, not once per schema file; don't build the db_pool twice 2014-08-27 11:45:16 +01:00
Kegan Dougal
a8774cf351 Merge branch 'client_server_url_rename' into develop 2014-08-27 11:38:13 +01:00
Kegan Dougal
135a1aa229 Final url modifications: renamed /presence_list to /presence/list to keep the top-level namespace clean. Updated tests. 2014-08-27 11:37:53 +01:00
Mark Haines
474dcecb11 Remove unused populate_previous_pdus 2014-08-27 11:34:31 +01:00
Kegan Dougal
dd661769e1 Renamed /rooms to /createRoom. Removed ability to PUT raw room IDs, and removed tests which tested that. Updated cmdclient and webclient. 2014-08-27 11:33:56 +01:00
Mark Haines
bf05218c4b Merge branch 'develop' into storage_transactions 2014-08-27 11:19:37 +01:00
Kegan Dougal
c65885e166 Added support for GET /events/$eventid with auth checks. 2014-08-27 10:33:01 +01:00
Kegan Dougal
dfa0cd1d90 Modified /join/$identifier to support $identifier being a room ID in addition to a room alias. 2014-08-27 09:43:42 +01:00
Mark Haines
d2798de660 Fold federation/handler into handlers/federation 2014-08-26 19:49:42 +01:00
Erik Johnston
67c5f89244 Enable presence again. Fix up api to match old api. 2014-08-26 19:40:29 +01:00
Erik Johnston
c1cf0b334e Fix exceptions so that the event stream works. Presence like events are turned off currently. 2014-08-26 19:18:11 +01:00
Erik Johnston
93cff1668c Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor 2014-08-26 18:57:55 +01:00
Erik Johnston
3a2a5b959c WIP: Completely change how event streaming and pagination work. This reflects the change in the underlying storage model. 2014-08-26 18:57:46 +01:00
Mark Haines
6966971a28 Use store.persist_event rather than pdu_actions.persist_outgoing/pdu_actions.persist_received 2014-08-26 18:57:17 +01:00
Mark Haines
a498df0428 Move new event boilerplate in room handlers into a method on a base clase. 2014-08-26 18:49:51 +01:00
Mark Haines
64e2a5d58e Move pdu and event persistence into a single persist_event function 2014-08-26 18:01:36 +01:00
Kegan Dougal
f84ddc75cb Pepper UT TODOs 2014-08-26 17:54:18 +01:00
Kegan Dougal
5dd8087ea4 Merge branch 'client_server_url_rename' into develop 2014-08-26 17:50:28 +01:00
Kegan Dougal
73a1022bca Merge branch 'develop' of github.com:matrix-org/synapse into client_server_url_rename 2014-08-26 17:50:08 +01:00
Kegan Dougal
5a3df1d029 Feedback: Removed FeedbackRestServlet. Modified keys on FeedbackEvent. Expanded the feedback constants to fully explain what type of feedback they are. 2014-08-26 17:49:46 +01:00
Kegan Dougal
6f0bba1934 Merge branch 'client_server_url_rename' into develop 2014-08-26 17:22:10 +01:00
Kegan Dougal
5a93bfe1f0 Removed MessageRestServlet, use RoomSendEventRestServlet instead. Updated cmdclient, tests and webclient. All appears to work. 2014-08-26 17:21:48 +01:00
Kegan Dougal
ad6d5ac06c Added RoomSendEventRestServlet to send generic non-state events. It even appears to work..! 2014-08-26 17:00:24 +01:00
Erik Johnston
8885c8546c Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor 2014-08-26 16:29:44 +01:00
Erik Johnston
9a93e83d90 Respect 'limit' param in initialSync api 2014-08-26 16:26:30 +01:00
Mark Haines
66a4d33524 Merge branch 'develop' into storage_transactions 2014-08-26 16:20:50 +01:00
Kegan Dougal
d0103400b5 Merge branch 'client_server_url_rename' into develop 2014-08-26 16:19:44 +01:00
Kegan Dougal
2e70de09b9 Renaming: /im/sync >> /initialSync. /rooms/$roomid/members/list >> /rooms/$roomid/members. /rooms$roomid/messages/list >> /room/$roomid/messages. Updated cmdclient, tests and webclient. 2014-08-26 16:19:17 +01:00
Mark Haines
47c1a3d454 Merge branch 'develop' into storage_transactions 2014-08-26 16:15:49 +01:00
Mark Haines
3281fec07a Use state_key rather than target_user_id 2014-08-26 16:14:54 +01:00
Mark Haines
a29d12a18a Use state_key rather than target_user_id 2014-08-26 16:13:32 +01:00
Mark Haines
4b63b06cad Merge branch 'develop' into storage_transactions
Conflicts:
	synapse/api/auth.py
	synapse/handlers/room.py
	synapse/storage/__init__.py
2014-08-26 16:07:05 +01:00
Erik Johnston
3df5cb804f Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor 2014-08-26 16:03:56 +01:00
Erik Johnston
b1e98ddc09 Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor 2014-08-26 16:03:25 +01:00
Mark Haines
ac21dfff6d Fix pyflakes errors 2014-08-26 16:02:59 +01:00
Mark Haines
32347bfcc9 fix a few pyflakes errors 2014-08-26 16:01:29 +01:00
Emmanuel ROHEE
bcf8eb687a Avoid double call of refresh at app startup 2014-08-26 16:57:41 +02:00
Kegan Dougal
0e7a41dc99 Merge branch 'client_server_url_rename' into develop 2014-08-26 15:55:01 +01:00
Kegan Dougal
8bd55cfdcb Fix ALL THE UNIT TESTS 2014-08-26 15:54:25 +01:00
Erik Johnston
ff3709e577 Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor 2014-08-26 15:45:03 +01:00
Emmanuel ROHEE
c21fcb3373 Determine and send user presence state 2014-08-26 16:25:27 +02:00
Kegan Dougal
b07bc9bdbd Merge branch 'client_server_url_rename' into develop 2014-08-26 15:06:24 +01:00
Kegan Dougal
27979028b2 Merge branch 'develop' of github.com:matrix-org/synapse into client_server_url_rename 2014-08-26 14:59:54 +01:00
Kegan Dougal
9ff9caeb74 webclient: Updated to use /rooms/$roomid/[invite|join|leave] 2014-08-26 14:59:31 +01:00
Kegan Dougal
5c0be8fde3 Implemented /rooms/$roomid/[invite|join|leave] with POST / PUT (incl txn ids) 2014-08-26 14:49:44 +01:00
Mark Haines
4b2ad549d5 Move the event storage into a single transaction 2014-08-26 14:36:03 +01:00
Kegan Dougal
732d954f89 Added basic in-memory REST transaction storage. Only the latest transaction for a given path/access_token combo is stored in order to prevent storing ALL request/response pairs. 2014-08-26 14:13:32 +01:00
Erik Johnston
485bb64ddb Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor 2014-08-26 13:55:37 +01:00
Erik Johnston
1291ac93f3 Add the ability to turn on the twisted manhole telnet service. 2014-08-26 13:43:55 +01:00
Erik Johnston
a664ec20e0 Add a do_users_share_a_room method and use that in the presence handler. 2014-08-26 13:38:26 +01:00
Kegan Dougal
7d79021c42 Added servlet for /rooms/$roomid/[invite|join|leave] 2014-08-26 12:54:43 +01:00
Kegan Dougal
f6daa9f170 Merge branch 'client_server_url_rename' into develop 2014-08-26 10:37:31 +01:00
Kegan Dougal
b01aeac842 URL renaming: Room state keys now use the format /rooms/$roomid/state/$event_type/$state_key. cmdclient: Fixed double urlencoding on leave command. Stream from the END since START now produces an error on /events. 2014-08-26 10:33:32 +01:00
Kegan Dougal
5796232cb1 Adjusted webclient to use new state paths. Updated membership msg template to actually show the person invited. Factored out common membership functions in matrix service. 2014-08-26 10:24:47 +01:00
Kegan Dougal
52b64617f9 Merge branch 'develop' of github.com:matrix-org/synapse into client_server_url_rename 2014-08-26 10:04:26 +01:00
Erik Johnston
fea7b60cf3 Add 'state_key' to valid_keys 2014-08-26 09:40:58 +01:00
Erik Johnston
b52b33acf6 Send down state_key to clients 2014-08-26 09:40:29 +01:00
Kegan Dougal
47c3a089c5 Merge branch 'develop' of github.com:matrix-org/synapse into client_server_url_rename 2014-08-26 09:26:33 +01:00
Kegan Dougal
cab3095803 Removed member list servlet: now using generic state paths. 2014-08-26 09:26:07 +01:00
Erik Johnston
be6abdff19 Order 'get_recent_events_for_room' correctly. 2014-08-26 09:22:58 +01:00
Emmanuel ROHEE
95839212a7 The landing URL is now '#/' which actually points to homeController 2014-08-25 11:35:33 +02:00
Emmanuel ROHEE
66d752dd1b Merge remote-tracking branch 'origin/master' into develop 2014-08-25 11:26:29 +02:00
Emmanuel ROHEE
1bd380c816 Merge remote-tracking branch 'origin/hotfixes-0.0.1' into develop 2014-08-25 11:13:54 +02:00
Emmanuel ROHEE
8b0473d5b9 Oops. Removed my NetBeans private folders 2014-08-25 10:25:43 +02:00
Erik Johnston
2c4908ed26 Ensure that we don't have duplicate hosts in the pdu destinations list 2014-08-24 14:35:13 +01:00
Erik Johnston
4521c2d277 Merge branch 'hotfixes-0.0.1' of github.com:matrix-org/synapse 2014-08-24 12:17:59 +01:00
Erik Johnston
0c3b4a1f63 For the content repo, don't just use homeserver.hostname as that might not include the port due to SRV. 2014-08-24 11:56:55 +01:00
Erik Johnston
9d86c8c7a6 Add a unique constraint on the room hosts table 2014-08-24 11:29:29 +01:00
Erik Johnston
a9a5329a11 Encode unicode from json as utf-8. This was required to allow people to register on my laptop 2014-08-24 11:29:29 +01:00
Matthew Hodgson
3f08a7ad21 oops 2014-08-23 20:48:14 +01:00
Matthew Hodgson
d2bb28d2df very quick and dirty responsive design for iPhones 2014-08-23 20:45:00 +01:00
Matthew Hodgson
45e70a6b70 point out the non-quick-start guide 2014-08-23 00:50:49 +01:00
Emmanuel ROHEE
31e7cec486 Added "Your name" as placeholder to help user understand what is this alone input box 2014-08-22 18:23:38 +02:00
Emmanuel ROHEE
41d1db2d4a Merge branch 'settings-page' into develop 2014-08-22 18:18:27 +02:00
Emmanuel ROHEE
de0706493a Use /home everywhere 2014-08-22 18:08:03 +02:00
Emmanuel ROHEE
4c7df52360 renamed rooms to home - renamed files 2014-08-22 18:01:08 +02:00
Mark Haines
1379dcae6f Take a snapshot of the state of the room before performing updates 2014-08-22 17:00:10 +01:00
Emmanuel ROHEE
61cac4df6e renamed rooms to home 2014-08-22 17:59:48 +02:00
Emmanuel ROHEE
aaf623fa53 Move profile parts of the rooms page and the config content into a new page: settings 2014-08-22 17:55:05 +02:00
Kegan Dougal
f690b7b827 Impl: /rooms/roomid/state/eventtype/state_key - Renamed RoomTopicRestServlet to RoomStateEventRestServlet. Support generic state event sending. 2014-08-22 15:59:15 +01:00
Erik Johnston
81a95937de Use new StreamToken in pagination config 2014-08-21 11:01:33 +01:00
Erik Johnston
7bec359408 Add in StreamToken type 2014-08-21 11:01:33 +01:00
103 changed files with 5934 additions and 3281 deletions

9
.gitignore vendored
View File

@@ -10,10 +10,17 @@ docs/build/
*.egg-info
cmdclient_config.json
homeserver.db
homeserver*.db
.coverage
htmlcov
demo/*.db
demo/*.log
demo/*.pid
graph/*.svg
graph/*.png
graph/*.dot
uploads

View File

@@ -1,3 +1,25 @@
Changes in synapse 0.1.0 (2014-08-29)
=====================================
Presence has been reenabled in this release.
Homeserver:
* Update client to server API, including:
- Use a more consistent url scheme.
- Provide more useful information in the initial sync api.
* Change the presence handling to be much more efficient.
* Change the presence server to server API to not require explicit polling of
all users who share a room with a user.
* Fix races in the event streaming logic.
Webclient:
* Update to use new client to server API.
* Add basic VOIP support.
* Add idle timers that change your status to away.
* Add recent rooms column when viewing a room.
* Various network efficiency improvements.
* Add basic mobile browser support.
* Add a settings page.
Changes in synapse 0.0.1 (2014-08-22)
=====================================
Presence has been disabled in this release due to a bug that caused the

View File

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

View File

@@ -1 +1 @@
0.0.1
0.1.0

View File

@@ -61,7 +61,7 @@ class SynapseCmd(cmd.Cmd):
"send_delivery_receipts": "on"
}
self.path_prefix = "/matrix/client/api/v1"
self.event_stream_token = "START"
self.event_stream_token = "END"
self.prompt = ">>> "
def do_EOF(self, line): # allows CTRL+D quitting
@@ -225,8 +225,13 @@ class SynapseCmd(cmd.Cmd):
json_res = yield self.http_client.do_request("GET", url)
print json_res
if ("type" not in json_res or "m.login.password" != json_res["type"] or
"stages" in 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)
@@ -402,19 +407,16 @@ class SynapseCmd(cmd.Cmd):
"""Leaves a room: "leave <roomid>" """
try:
args = self._parse(line, ["roomid"], force_keys=True)
path = ("/rooms/%s/members/%s/state" %
(urllib.quote(args["roomid"]), self._usr()))
reactor.callFromThread(self._run_and_pprint, "DELETE", path)
self._do_membership_change(args["roomid"], "leave", self._usr())
except Exception as e:
print e
def do_send(self, line):
"""Sends a message. "send <roomid> <body>" """
args = self._parse(line, ["roomid", "body"])
msg_id = "m%s" % int(time.time())
path = "/rooms/%s/messages/%s/%s" % (urllib.quote(args["roomid"]),
self._usr(),
msg_id)
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"]
@@ -438,7 +440,7 @@ class SynapseCmd(cmd.Cmd):
print "Unrecognised type: %s" % args["type"]
return
room_id = args["roomid"]
path = "/rooms/%s/%s/list" % (urllib.quote(room_id), args["type"])
path = "/rooms/%s/%s" % (urllib.quote(room_id), args["type"])
qp = {"access_token": self._tok()}
if "qp" in args:
@@ -474,7 +476,7 @@ class SynapseCmd(cmd.Cmd):
room_name = args["vis"]
body["room_alias_name"] = room_name
reactor.callFromThread(self._run_and_pprint, "POST", "/rooms", body)
reactor.callFromThread(self._run_and_pprint, "POST", "/createRoom", body)
def do_raw(self, line):
"""Directly send a JSON object: "raw <method> <path> <data> <notoken>"
@@ -567,7 +569,7 @@ class SynapseCmd(cmd.Cmd):
alt_text="Sent receipt for %s" % event["msg_id"])
def _do_membership_change(self, roomid, membership, userid):
path = "/rooms/%s/members/%s/state" % (urllib.quote(roomid), userid)
path = "/rooms/%s/state/m.room.member/%s" % (urllib.quote(roomid), urllib.quote(userid))
data = {
"membership": membership
}

View File

@@ -6,7 +6,7 @@ CWD=$(pwd)
cd "$DIR/.."
for port in "8080" "8081" "8082"; do
for port in 8080 8081 8082; do
echo "Starting server on port $port... "
python -m synapse.app.homeserver \
@@ -15,7 +15,8 @@ for port in "8080" "8081" "8082"; do
-f "$DIR/$port.log" \
-d "$DIR/$port.db" \
-vv \
-D --pid-file "$DIR/$port.pid"
-D --pid-file "$DIR/$port.pid" \
--manhole $((port + 1000))
done
echo "Starting webclient on port 8000..."

View File

@@ -21,6 +21,14 @@
{
"path": "/presence",
"description": "Presence operations"
},
{
"path": "/events",
"description": "Event operations"
},
{
"path": "/directory",
"description": "Directory operations"
}
],
"authorizations": {

View File

@@ -0,0 +1,83 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"resourcePath": "/directory",
"produces": [
"application/json"
],
"apis": [
{
"path": "/directory/room/{roomAlias}",
"operations": [
{
"method": "GET",
"summary": "Get the room ID corresponding to this room alias.",
"type": "DirectoryResponse",
"nickname": "get_room_id_for_alias",
"parameters": [
{
"name": "roomAlias",
"description": "The room alias.",
"required": true,
"type": "string",
"paramType": "path"
}
]
},
{
"method": "PUT",
"summary": "Create a new mapping from room alias to room ID.",
"type": "void",
"nickname": "add_room_alias",
"parameters": [
{
"name": "roomAlias",
"description": "The room alias to set.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "body",
"description": "The room ID to set.",
"required": true,
"type": "RoomAliasRequest",
"paramType": "body"
}
]
}
]
}
],
"models": {
"DirectoryResponse": {
"id": "DirectoryResponse",
"properties": {
"room_id": {
"type": "string",
"description": "The fully-qualified room ID.",
"required": true
},
"servers": {
"type": "array",
"items": {
"$ref": "string"
},
"description": "A list of servers that know about this room.",
"required": true
}
}
},
"RoomAliasRequest": {
"id": "RoomAliasRequest",
"properties": {
"room_id": {
"type": "string",
"description": "The room ID to map the alias to.",
"required": true
}
}
}
}
}

View File

@@ -1,299 +1,246 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://petstore.swagger.wordnik.com/api",
"resourcePath": "/user",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"resourcePath": "/events",
"produces": [
"application/json"
],
"apis": [
{
"path": "/user",
"operations": [
{
"method": "POST",
"summary": "Create user",
"notes": "This can only be done by the logged in user.",
"type": "void",
"nickname": "createUser",
"authorizations": {
"oauth2": [
{
"scope": "test:anything",
"description": "anything"
}
]
},
"parameters": [
{
"name": "body",
"description": "Created user object",
"required": true,
"type": "User",
"paramType": "body"
}
]
}
]
},
{
"path": "/user/logout",
"path": "/events",
"operations": [
{
"method": "GET",
"summary": "Logs out current logged in user session",
"notes": "",
"type": "void",
"nickname": "logoutUser",
"authorizations": {},
"parameters": []
"summary": "Listen on the event stream",
"notes": "This can only be done by the logged in user. This will block until an event is received, or until the timeout is reached.",
"type": "PaginationChunk",
"nickname": "get_event_stream"
}
]
},
{
"path": "/user/createWithArray",
"operations": [
],
"parameters": [
{
"method": "POST",
"summary": "Creates list of users with given input array",
"notes": "",
"type": "void",
"nickname": "createUsersWithArrayInput",
"authorizations": {
"oauth2": [
{
"scope": "test:anything",
"description": "anything"
}
]
},
"parameters": [
{
"name": "body",
"description": "List of user object",
"required": true,
"type": "array",
"items": {
"$ref": "User"
},
"paramType": "body"
}
]
}
]
},
{
"path": "/user/createWithList",
"operations": [
{
"method": "POST",
"summary": "Creates list of users with given list input",
"notes": "",
"type": "void",
"nickname": "createUsersWithListInput",
"authorizations": {
"oauth2": [
{
"scope": "test:anything",
"description": "anything"
}
]
},
"parameters": [
{
"name": "body",
"description": "List of user object",
"required": true,
"type": "array",
"items": {
"$ref": "User"
},
"paramType": "body"
}
]
}
]
},
{
"path": "/user/{username}",
"operations": [
{
"method": "PUT",
"summary": "Updated user",
"notes": "This can only be done by the logged in user.",
"type": "void",
"nickname": "updateUser",
"authorizations": {
"oauth2": [
{
"scope": "test:anything",
"description": "anything"
}
]
},
"parameters": [
{
"name": "username",
"description": "name that need to be deleted",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "body",
"description": "Updated user object",
"required": true,
"type": "User",
"paramType": "body"
}
],
"responseMessages": [
{
"code": 400,
"message": "Invalid username supplied"
},
{
"code": 404,
"message": "User not found"
}
]
},
{
"method": "DELETE",
"summary": "Delete user",
"notes": "This can only be done by the logged in user.",
"type": "void",
"nickname": "deleteUser",
"authorizations": {
"oauth2": [
{
"scope": "test:anything",
"description": "anything"
}
]
},
"parameters": [
{
"name": "username",
"description": "The name that needs to be deleted",
"required": true,
"type": "string",
"paramType": "path"
}
],
"responseMessages": [
{
"code": 400,
"message": "Invalid username supplied"
},
{
"code": 404,
"message": "User not found"
}
]
},
{
"method": "GET",
"summary": "Get user by user name",
"notes": "",
"type": "User",
"nickname": "getUserByName",
"authorizations": {},
"parameters": [
{
"name": "username",
"description": "The name that needs to be fetched. Use user1 for testing.",
"required": true,
"type": "string",
"paramType": "path"
}
],
"responseMessages": [
{
"code": 400,
"message": "Invalid username supplied"
},
{
"code": 404,
"message": "User not found"
}
]
}
]
},
{
"path": "/user/login",
"operations": [
{
"method": "GET",
"summary": "Logs user into the system",
"notes": "",
"name": "from",
"description": "The token to stream from.",
"required": false,
"type": "string",
"nickname": "loginUser",
"authorizations": {},
"paramType": "query"
},
{
"name": "timeout",
"description": "The maximum time in milliseconds to wait for an event.",
"required": false,
"type": "integer",
"paramType": "query"
}
],
"responseMessages": [
{
"code": 400,
"message": "Bad pagination token."
}
]
},
{
"path": "/events/{eventId}",
"operations": [
{
"method": "GET",
"summary": "Get information about a single event.",
"notes": "Get information about a single event.",
"type": "Event",
"nickname": "get_event",
"parameters": [
{
"name": "username",
"description": "The user name for login",
"name": "eventId",
"description": "The event ID to get.",
"required": true,
"type": "string",
"paramType": "query"
},
{
"name": "password",
"description": "The password for login in clear text",
"required": true,
"type": "string",
"paramType": "query"
"paramType": "path"
}
],
"responseMessages": [
{
"code": 400,
"message": "Invalid username and password combination"
"code": 404,
"message": "Event not found."
}
]
}
]
},
{
"path": "/initialSync",
"operations": [
{
"method": "GET",
"summary": "Get this user's current state.",
"notes": "Get this user's current state.",
"type": "InitialSyncResponse",
"nickname": "initial_sync",
"parameters": [
{
"name": "limit",
"description": "The maximum number of messages to return for each room.",
"type": "integer",
"paramType": "query",
"required": false
}
]
}
]
},
{
"path": "/publicRooms",
"operations": [
{
"method": "GET",
"summary": "Get a list of publicly visible rooms.",
"type": "PublicRoomsPaginationChunk",
"nickname": "get_public_room_list"
}
]
}
],
"models": {
"User": {
"id": "User",
"PaginationChunk": {
"id": "PaginationChunk",
"properties": {
"id": {
"type": "integer",
"format": "int64"
"start": {
"type": "string",
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
"required": true
},
"firstName": {
"type": "string"
"end": {
"type": "string",
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
"required": true
},
"username": {
"type": "string"
"chunk": {
"type": "array",
"description": "An array of events.",
"required": true,
"items": {
"$ref": "Event"
}
}
}
},
"Event": {
"id": "Event",
"properties": {
"event_id": {
"type": "string",
"description": "An ID which uniquely identifies this event.",
"required": true
},
"lastName": {
"type": "string"
"room_id": {
"type": "string",
"description": "The room in which this event occurred.",
"required": true
}
}
},
"PublicRoomInfo": {
"id": "PublicRoomInfo",
"properties": {
"aliases": {
"type": "array",
"description": "A list of room aliases for this room.",
"items": {
"$ref": "string"
}
},
"email": {
"type": "string"
"name": {
"type": "string",
"description": "The name of the room, as given by the m.room.name state event."
},
"password": {
"type": "string"
"room_id": {
"type": "string",
"description": "The room ID for this public room.",
"required": true
},
"phone": {
"type": "string"
"topic": {
"type": "string",
"description": "The topic of this room, as given by the m.room.topic state event."
}
}
},
"PublicRoomsPaginationChunk": {
"id": "PublicRoomsPaginationChunk",
"properties": {
"start": {
"type": "string",
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
"required": true
},
"userStatus": {
"type": "integer",
"format": "int32",
"description": "User Status",
"enum": [
"1-registered",
"2-active",
"3-closed"
]
"end": {
"type": "string",
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
"required": true
},
"chunk": {
"type": "array",
"description": "A list of public room data.",
"required": true,
"items": {
"$ref": "PublicRoomInfo"
}
}
}
},
"InitialSyncResponse": {
"id": "InitialSyncResponse",
"properties": {
"end": {
"type": "string",
"description": "A streaming token which can be used with /events to continue from this snapshot of data.",
"required": true
},
"presence": {
"type": "array",
"description": "A list of presence events.",
"items": {
"$ref": "Event"
},
"required": false
},
"rooms": {
"type": "array",
"description": "A list of initial sync room data.",
"required": false,
"items": {
"$ref": "InitialSyncRoomData"
}
}
}
},
"InitialSyncRoomData": {
"id": "InitialSyncRoomData",
"properties": {
"membership": {
"type": "string",
"description": "This user's membership state in this room.",
"required": true
},
"room_id": {
"type": "string",
"description": "The ID of this room.",
"required": true
},
"messages": {
"type": "PaginationChunk",
"description": "The most recent messages for this room, governed by the limit parameter.",
"required": false
},
"state": {
"type": "array",
"description": "A list of state events representing the current state of the room.",
"required": false,
"items": {
"$ref": "Event"
}
}
}
}
}
}
}

View File

@@ -55,7 +55,7 @@
]
},
{
"path": "/presence_list/{userId}",
"path": "/presence/list/{userId}",
"operations": [
{
"method": "GET",

View File

@@ -14,13 +14,103 @@
},
"apis": [
{
"path": "/rooms/{roomId}/messages/{userId}/{messageId}",
"path": "/rooms/{roomId}/send/{eventType}/{txnId}",
"operations": [
{
"method": "PUT",
"summary": "Send a generic non-state event to this room.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/{eventType}",
"type": "EventId",
"nickname": "send_non_state_event",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "body",
"description": "The event contents",
"required": true,
"type": "EventContent",
"paramType": "body"
},
{
"name": "roomId",
"description": "The room to send the message in.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "eventType",
"description": "The type of event to send.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "txnId",
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
"required": true,
"type": "string",
"paramType": "path"
}
]
}
]
},
{
"path": "/rooms/{roomId}/state/{eventType}/{stateKey}",
"operations": [
{
"method": "PUT",
"summary": "Send a generic state event to this room.",
"notes": "The state key can be omitted, such that you can PUT to /rooms/{roomId}/state/{eventType}. The state key defaults to a 0 length string in this case.",
"type": "void",
"nickname": "send_state_event",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "body",
"description": "The event contents",
"required": true,
"type": "EventContent",
"paramType": "body"
},
{
"name": "roomId",
"description": "The room to send the message in.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "eventType",
"description": "The type of event to send.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "stateKey",
"description": "An identifier used to specify clobbering semantics. State events with the same (roomId, eventType, stateKey) will be replaced.",
"required": true,
"type": "string",
"paramType": "path"
}
]
}
]
},
{
"path": "/rooms/{roomId}/send/m.room.message/{txnId}",
"operations": [
{
"method": "PUT",
"summary": "Send a message in this room.",
"notes": "Send a message in this room.",
"type": "void",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message",
"type": "EventId",
"nickname": "send_message",
"consumes": [
"application/json"
@@ -41,67 +131,18 @@
"paramType": "path"
},
{
"name": "userId",
"description": "The fully qualified message sender's user ID.",
"name": "txnId",
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "messageId",
"description": "A message ID which is unique for each room and user.",
"required": true,
"type": "string",
"paramType": "path"
}
],
"responseMessages": [
{
"code": 403,
"message": "Must send messages as yourself."
}
]
},
{
"method": "GET",
"summary": "Get a message from this room.",
"notes": "Get a message from this room.",
"type": "Message",
"nickname": "get_message",
"parameters": [
{
"name": "roomId",
"description": "The room to send the message in.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "userId",
"description": "The fully qualified message sender's user ID.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "messageId",
"description": "A message ID which is unique for each room and user.",
"required": true,
"type": "string",
"paramType": "path"
}
],
"responseMessages": [
{
"code": 404,
"message": "Message not found."
}
]
}
]
},
{
"path": "/rooms/{roomId}/topic",
"path": "/rooms/{roomId}/state/m.room.topic",
"operations": [
{
"method": "PUT",
@@ -127,12 +168,6 @@
"type": "string",
"paramType": "path"
}
],
"responseMessages": [
{
"code": 403,
"message": "Must send messages as yourself."
}
]
},
{
@@ -160,13 +195,13 @@
]
},
{
"path": "/rooms/{roomId}/messages/{msgSenderId}/{messageId}/feedback/{senderId}/{feedbackType}",
"path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}",
"operations": [
{
"method": "PUT",
"summary": "Send feedback to a message.",
"notes": "Send feedback to a message.",
"type": "void",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback",
"type": "EventId",
"nickname": "send_feedback",
"consumes": [
"application/json"
@@ -187,107 +222,124 @@
"paramType": "path"
},
{
"name": "msgSenderId",
"description": "The fully qualified message sender's user ID.",
"name": "txnId",
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "messageId",
"description": "A message ID which is unique for each room and user.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "senderId",
"description": "The fully qualified feedback sender's user ID.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "feedbackType",
"description": "The type of feedback being sent.",
"required": true,
"type": "string",
"paramType": "path",
"enum": [
"d",
"r"
]
}
],
"responseMessages": [
{
"code": 403,
"message": "Must send feedback as yourself."
},
{
"code": 400,
"message": "Bad feedback type."
}
]
},
{
"method": "GET",
"summary": "Get feedback for a message.",
"notes": "Get feedback for a message.",
"type": "Feedback",
"nickname": "get_feedback",
"parameters": [
{
"name": "roomId",
"description": "The room to send the message in.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "msgSenderId",
"description": "The fully qualified message sender's user ID.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "messageId",
"description": "A message ID which is unique for each room and user.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "senderId",
"description": "The fully qualified feedback sender's user ID.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "feedbackType",
"description": "Enum: The type of feedback being sent.",
"required": true,
"type": "string",
"paramType": "path",
"enum": [
"d",
"r"
]
}
],
"responseMessages": [
{
"code": 404,
"message": "Feedback not found."
}
]
}
]
},
{
"path": "/rooms/{roomId}/members/{userId}/state",
"path": "/rooms/{roomId}/invite/{txnId}",
"operations": [
{
"method": "PUT",
"summary": "Invite a user to this room.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/invite",
"type": "void",
"nickname": "invite",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "roomId",
"description": "The room which has this user.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "txnId",
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
"required": false,
"type": "string",
"paramType": "path"
},
{
"name": "body",
"description": "The user to invite.",
"required": true,
"type": "InviteRequest",
"paramType": "body"
}
]
}
]
},
{
"path": "/rooms/{roomId}/join/{txnId}",
"operations": [
{
"method": "PUT",
"summary": "Join this room.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/join",
"type": "void",
"nickname": "join_room",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "roomId",
"description": "The room to join.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "txnId",
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
"required": false,
"type": "string",
"paramType": "path"
}
]
}
]
},
{
"path": "/rooms/{roomId}/leave/{txnId}",
"operations": [
{
"method": "PUT",
"summary": "Leave this room.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/leave",
"type": "void",
"nickname": "leave",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "roomId",
"description": "The room to leave.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "txnId",
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
"required": false,
"type": "string",
"paramType": "path"
}
]
}
]
},
{
"path": "/rooms/{roomId}/state/m.room.member/{userId}",
"operations": [
{
"method": "PUT",
@@ -376,58 +428,25 @@
"message": "Member not found."
}
]
},
{
"method": "DELETE",
"summary": "Leave a room.",
"notes": "Leave a room.",
"type": "void",
"nickname": "remove_membership",
"parameters": [
{
"name": "userId",
"description": "The user who is leaving.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "roomId",
"description": "The room which has this user.",
"required": true,
"type": "string",
"paramType": "path"
}
],
"responseMessages": [
{
"code": 403,
"message": "You are not in the room."
},
{
"code": 403,
"message": "Cannot force another user to leave."
}
]
}
]
},
{
"path": "/join/{roomAlias}",
"path": "/join/{roomAliasOrId}",
"operations": [
{
"method": "PUT",
"summary": "Join a room via a room alias.",
"notes": "Join a room via a room alias.",
"summary": "Join a room via a room alias or room ID.",
"notes": "Join a room via a room alias or room ID.",
"type": "RoomInfo",
"nickname": "join_room_via_alias",
"nickname": "join",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "roomAlias",
"description": "The room alias to join.",
"name": "roomAliasOrId",
"description": "The room alias or room ID to join.",
"required": true,
"type": "string",
"paramType": "path"
@@ -443,7 +462,7 @@
]
},
{
"path": "/rooms",
"path": "/createRoom",
"operations": [
{
"method": "POST",
@@ -477,7 +496,7 @@
]
},
{
"path": "/rooms/{roomId}/messages/list",
"path": "/rooms/{roomId}/messages",
"operations": [
{
"method": "GET",
@@ -519,7 +538,7 @@
]
},
{
"path": "/rooms/{roomId}/members/list",
"path": "/rooms/{roomId}/members",
"operations": [
{
"method": "GET",
@@ -559,6 +578,51 @@
]
}
]
},
{
"path": "/rooms/{roomId}/state",
"operations": [
{
"method": "GET",
"summary": "Get a list of all the current state events for this room.",
"notes": "Get a list of all the current state events for this room.",
"type": "array",
"items": {
"$ref": "Event"
},
"nickname": "get_state_events",
"parameters": [
{
"name": "roomId",
"description": "The room to get a list of current state events from.",
"required": true,
"type": "string",
"paramType": "path"
}
]
}
]
},
{
"path": "/rooms/{roomId}/initialSync",
"operations": [
{
"method": "GET",
"summary": "Get all the current information for this room, including messages and state events.",
"notes": "Get all the current information for this room, including messages and state events.",
"type": "InitialSyncRoomData",
"nickname": "get_room_sync_data",
"parameters": [
{
"name": "roomId",
"description": "The room to get information for.",
"required": true,
"type": "string",
"paramType": "path"
}
]
}
]
}
],
"models": {
@@ -704,12 +768,17 @@
"properties": {
"event_id": {
"type": "string",
"description": "An ID which uniquely identifies this event.",
"description": "An ID which uniquely identifies this event. This is automatically set by the server.",
"required": true
},
"room_id": {
"type": "string",
"description": "The room in which this event occurred.",
"description": "The room in which this event occurred. This is automatically set by the server.",
"required": true
},
"type": {
"type": "string",
"description": "The event type.",
"required": true
}
},
@@ -717,6 +786,26 @@
"MessageEvent"
]
},
"EventId": {
"id": "EventId",
"properties": {
"event_id": {
"type": "string",
"description": "The allocated event ID for this event.",
"required": true
}
}
},
"EventContent": {
"id": "EventContent",
"properties": {
"__event_content_keys__": {
"type": "string",
"description": "Event-specific content keys and values.",
"required": false
}
}
},
"MessageEvent": {
"id": "MessageEvent",
"properties": {
@@ -733,73 +822,40 @@
}
}
},
"Tag": {
"id": "Tag",
"InviteRequest": {
"id": "InviteRequest",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
}
},
"Pet": {
"id": "Pet",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64",
"description": "unique identifier for the pet",
"minimum": "0.0",
"maximum": "100.0"
},
"category": {
"$ref": "Category"
},
"name": {
"type": "string"
},
"photoUrls": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"type": "array",
"items": {
"$ref": "Tag"
}
},
"status": {
"user_id": {
"type": "string",
"description": "pet status in the store",
"enum": [
"available",
"pending",
"sold"
]
"description": "The fully-qualified user ID."
}
}
},
"Category": {
"id": "Category",
"InitialSyncRoomData": {
"id": "InitialSyncRoomData",
"properties": {
"id": {
"type": "integer",
"format": "int64"
"membership": {
"type": "string",
"description": "This user's membership state in this room.",
"required": true
},
"name": {
"type": "string"
"room_id": {
"type": "string",
"description": "The ID of this room.",
"required": true
},
"pet": {
"$ref": "Pet"
"messages": {
"type": "MessagePaginationChunk",
"description": "The most recent messages for this room, governed by the limit parameter.",
"required": false
},
"state": {
"type": "array",
"description": "A list of state events representing the current state of the room.",
"required": false,
"items": {
"$ref": "Event"
}
}
}
}

View File

@@ -1,92 +0,0 @@
=========================
Client-Server URL Summary
=========================
A brief overview of the URL scheme involved in the Synapse Client-Server API.
URLs
====
Fetch events:
GET /events
Registering an account
POST /register
Unregistering an account
POST /unregister
Rooms
-----
Creating a room by ID
PUT /rooms/$roomid
Creating an anonymous room
POST /rooms
Room topic
GET /rooms/$roomid/topic
PUT /rooms/$roomid/topic
List rooms
GET /rooms/list
Invite/Join/Leave
GET /rooms/$roomid/members/$userid/state
PUT /rooms/$roomid/members/$userid/state
DELETE /rooms/$roomid/members/$userid/state
List members
GET /rooms/$roomid/members/list
Sending/reading messages
PUT /rooms/$roomid/messages/$sender/$msgid
Feedback
GET /rooms/$roomid/messages/$sender/$msgid/feedback/$feedbackuser/$feedback
PUT /rooms/$roomid/messages/$sender/$msgid/feedback/$feedbackuser/$feedback
Paginating messages
GET /rooms/$roomid/messages/list
Profiles
--------
Display name
GET /profile/$userid/displayname
PUT /profile/$userid/displayname
Avatar URL
GET /profile/$userid/avatar_url
PUT /profile/$userid/avatar_url
Metadata
GET /profile/$userid/metadata
POST /profile/$userid/metadata
Presence
--------
My state or status message
GET /presence/$userid/status
PUT /presence/$userid/status
also 'GET' for fetching others
TODO(paul): per-device idle time, device type; similar to above
My presence list
GET /presence_list/$myuserid
POST /presence_list/$myuserid
body is JSON-encoded dict of keys:
invite: list of UserID strings to invite
drop: list of UserID strings to remove
TODO(paul): define other ops: accept, group management, ordering?
Presence polling start/stop
POST /presence_list/$myuserid?op=start
POST /presence_list/$myuserid?op=stop
Presence invite
POST /presence_list/$myuserid/invite/$targetuserid

839
docs/specification.rst Normal file
View File

@@ -0,0 +1,839 @@
Matrix Specification
====================
TODO(Introduction) : Matthew
- Similar to intro paragraph from README.
- Explaining the overall mission, what this spec describes...
- "What is Matrix?"
- Draw parallels with email?
Architecture
============
Clients transmit data to other clients through home servers (HSes). Clients do not communicate with each
other directly.
::
How data flows between clients
==============================
{ Matrix client A } { Matrix client B }
^ | ^ |
| events | | events |
| V | V
+------------------+ +------------------+
| |---------( HTTP )---------->| |
| Home Server | | Home Server |
| |<--------( HTTP )-----------| |
+------------------+ Federation +------------------+
A "Client" is an end-user, typically a human using a web application or mobile app. Clients use the
"Client-to-Server" (C-S) API to communicate with their home server. A single Client is usually
responsible for a single user account. A user account is represented by their "User ID". This ID is
namespaced to the home server which allocated the account and looks like::
@localpart:domain
The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user.
A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes.
It is typically responsible for multiple clients. "Federation" is the term used to describe the
sharing of data between two or more home servers.
Data in Matrix is encapsulated in an "Event". An event is an action within the system. Typically each
action (e.g. sending a message) correlates with exactly one event. Each event has a ``type`` which is
used to differentiate different kinds of data. ``type`` values SHOULD be namespaced according to standard
Java package naming conventions, e.g. ``com.example.myapp.event``. Events are usually sent in the context
of a "Room".
Room structure
--------------
A room is a conceptual place where users can send and receive events. Rooms
can be created, joined and left. Events are sent to a room, and all
participants in that room will receive the event. Rooms are uniquely
identified via a "Room ID", which look like::
!opaque_id:domain
There is exactly one room ID for each room. Whilst the room ID does contain a
domain, it is simply for namespacing room IDs. The room does NOT reside on the
domain specified. Room IDs are not meant to be human readable.
The following diagram shows an ``m.room.message`` event being sent in the room
``!qporfwt:matrix.org``::
{ @alice:matrix.org } { @bob:domain.com }
| ^
| |
Room ID: !qporfwt:matrix.org Room ID: !qporfwt:matrix.org
Event type: m.room.message Event type: m.room.message
Content: { JSON object } Content: { JSON object }
| |
V |
+------------------+ +------------------+
| Home Server | | Home Server |
| matrix.org |<-------Federation------->| domain.com |
+------------------+ +------------------+
| ................................. |
|______| Partially Shared State |_______|
| Room ID: !qporfwt:matrix.org |
| Servers: matrix.org, domain.com |
| Members: |
| - @alice:matrix.org |
| - @bob:domain.com |
|.................................|
Federation maintains shared state between multiple home servers, such that when an event is
sent to a room, the home server knows where to forward the event on to, and how to process
the event. Home servers do not need to have completely shared state in order to participate
in a room. State is scoped to a single room, and federation ensures that all home servers
have the information they need, even if that means the home server has to request more
information from another home server before processing the event.
Room Aliases
------------
Each room can also have multiple "Room Aliases", which looks like::
#room_alias:domain
A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained
by visiting the domain specified. Room aliases are designed to be human readable strings
which can be used to publicise rooms. Note that the mapping from a room alias to a
room ID is not fixed, and may change over time to point to a different room ID. For this
reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on
subsequent requests.
::
GET
#matrix:domain.com !aaabaa:matrix.org
| ^
| |
_______V____________________|____
| domain.com |
| Mappings: |
| #matrix >> !aaabaa:matrix.org |
| #golf >> !wfeiofh:sport.com |
| #bike >> !4rguxf:matrix.org |
|________________________________|
Identity
--------
- Identity in relation to 3PIDs. Discovery of users based on 3PIDs.
- Identity servers; trusted clique of servers which replicate content.
- They govern the mapping of 3PIDs to user IDs and the creation of said mappings.
- Not strictly required in order to communicate.
API Standards
-------------
- All HTTP[S]
- Uses JSON as HTTP bodies
- Standard error response format { errcode: M_WHATEVER, error: "some message" }
- C-S API provides POST for operations, or PUT with txn IDs. Explain txn IDs.
Receiving live updates on a client
----------------------------------
- C-S longpoll event stream
- Concept of start/end tokens.
- Mention /initialSync to get token.
Rooms
=====
- How are they created? PDU anchor point: "root of the tree".
- Adding / removing aliases.
- Invite/join dance
- State and non-state data (+extensibility)
TODO : Room permissions / config / power levels.
Messages
========
This specification outlines several standard event types, all of which are
prefixed with ``m.``
State messages
--------------
- m.room.name
- m.room.topic
- m.room.member
- m.room.config
- m.room.invite_join
What are they, when are they used, what do they contain, how should they be used
Non-state messages
------------------
- m.room.message
- m.room.message.feedback (and compressed format)
What are they, when are they used, what do they contain, how should they be used
m.room.message msgtypes
-----------------------
Each ``m.room.message`` MUST have a ``msgtype`` key which identifies the type of
message being sent. Each type has their own required and optional keys, as outlined
below:
``m.text``
Required keys:
- ``body`` : "string" - The body of the message.
Optional keys:
None.
Example:
``{ "msgtype": "m.text", "body": "I am a fish" }``
``m.emote``
Required keys:
- ``body`` : "string" - The emote action to perform.
Optional keys:
None.
Example:
``{ "msgtype": "m.emote", "body": "tries to come up with a witty explanation" }``
``m.image``
Required keys:
- ``url`` : "string" - The URL to the image.
Optional keys:
- ``info`` : "string" - info : JSON object (ImageInfo) - The image info for image
referred to in ``url``.
- ``thumbnail_url`` : "string" - The URL to the thumbnail.
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
referred to in ``thumbnail_url``.
- ``body`` : "string" - The alt text of the image, or some kind of content
description for accessibility e.g. "image attachment".
ImageInfo:
Information about an image::
{
"size" : integer (size of image in bytes),
"w" : integer (width of image in pixels),
"h" : integer (height of image in pixels),
"mimetype" : "string (e.g. image/jpeg)",
}
``m.audio``
Required keys:
- ``url`` : "string" - The URL to the audio.
Optional keys:
- ``info`` : JSON object (AudioInfo) - The audio info for the audio referred to in
``url``.
- ``body`` : "string" - A description of the audio e.g. "Bee Gees -
Stayin' Alive", or some kind of content description for accessibility e.g.
"audio attachment".
AudioInfo:
Information about a piece of audio::
{
"mimetype" : "string (e.g. audio/aac)",
"size" : integer (size of audio in bytes),
"duration" : integer (duration of audio in milliseconds),
}
``m.video``
Required keys:
- ``url`` : "string" - The URL to the video.
Optional keys:
- ``info`` : JSON object (VideoInfo) - The video info for the video referred to in
``url``.
- ``body`` : "string" - A description of the video e.g. "Gangnam style",
or some kind of content description for accessibility e.g. "video attachment".
VideoInfo:
Information about a video::
{
"mimetype" : "string (e.g. video/mp4)",
"size" : integer (size of video in bytes),
"duration" : integer (duration of video in milliseconds),
"w" : integer (width of video in pixels),
"h" : integer (height of video in pixels),
"thumbnail_url" : "string (URL to image)",
"thumbanil_info" : JSON object (ImageInfo)
}
``m.location``
Required keys:
- ``geo_uri`` : "string" - The geo URI representing the location.
Optional keys:
- ``thumbnail_url`` : "string" - The URL to a thumnail of the location being
represented.
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
referred to in ``thumbnail_url``.
- ``body`` : "string" - A description of the location e.g. "Big Ben,
London, UK", or some kind of content description for accessibility e.g.
"location attachment".
The following keys can be attached to any ``m.room.message``:
Optional keys:
- ``sender_ts`` : integer - A timestamp (ms resolution) representing the
wall-clock time when the message was sent from the client.
Presence
========
Each user has the concept of presence information. This encodes the
"availability" of that user, suitable for display on other user's clients. This
is transmitted as an ``m.presence`` event and is one of the few events which
are sent *outside the context of a room*. The basic piece of presence information
is represented by the ``state`` key, which is an enum of one of the following:
- ``online`` : The default state when the user is connected to an event stream.
- ``unavailable`` : The user is not reachable at this time.
- ``offline`` : The user is not connected to an event stream.
- ``free_for_chat`` : The user is generally willing to receive messages
moreso than default.
- ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
state anyway and generally interact with client features.
This basic ``state`` field applies to the user as a whole, regardless of how many
client devices they have connected. The home server should synchronise this
status choice among multiple devices to ensure the user gets a consistent
experience.
Idle Time
---------
As well as the basic ``state`` field, the presence information can also show a sense
of an "idle timer". This should be maintained individually by the user's
clients, and the home server can take the highest reported time as that to
report. When a user is offline, the home server can still report when the user was last
seen online.
Transmission
------------
- Transmitted as an EDU.
- Presence lists determine who to send to.
Presence List
-------------
Each user's home server stores a "presence list" for that user. This stores a
list of other user IDs the user has chosen to add to it. To be added to this
list, the user being added must receive permission from the list owner. Once
granted, both user's HS(es) store this information. Since such subscriptions
are likely to be bidirectional, HSes may wish to automatically accept requests
when a reverse subscription already exists.
Presence and Permissions
------------------------
For a viewing user to be allowed to see the presence information of a target
user, either:
- The target user has allowed the viewing user to add them to their presence
list, or
- The two users share at least one room in common
In the latter case, this allows for clients to display some minimal sense of
presence information in a user list for a room.
Typing notifications
====================
TODO : Leo
Voice over IP
=============
TODO : Dave
Profiles
========
Internally within Matrix users are referred to by their user ID, which is not a
human-friendly string. Profiles grant users the ability to see human-readable
names for other users that are in some way meaningful to them. Additionally,
profiles can publish additional information, such as the user's age or location.
A Profile consists of a display name, an avatar picture, and a set of other
metadata fields that the user may wish to publish (email address, phone
numbers, website URLs, etc...). This specification puts no requirements on the
display name other than it being a valid unicode string.
- Metadata extensibility
- Bundled with which events? e.g. m.room.member
- Generate own events? What type?
Registration and login
======================
Clients must register with a home server in order to use Matrix. After
registering, the client will be given an access token which must be used in ALL
requests to that home server as a query parameter 'access_token'.
- TODO Kegan : Make registration like login (just omit the "user" key on the
initial request?)
If the client has already registered, they need to be able to login to their
account. The home server may provide many different ways of logging in, such
as user/password auth, login via a social network (OAuth2), login by confirming
a token sent to their email address, etc. This specification does not define how
home servers should authorise their users who want to login to their existing
accounts, but instead defines the standard interface which implementations
should follow so that ANY client can login to ANY home server.
The login process breaks down into the following:
1. Determine the requirements for logging in.
2. Submit the login stage credentials.
3. Get credentials or be told the next stage in the login process and repeat
step 2.
As each home server may have different ways of logging in, the client needs to know how
they should login. All distinct login stages MUST have a corresponding ``type``.
A ``type`` is a namespaced string which details the mechanism for logging in.
A client may be able to login via multiple valid login flows, and should choose a single
flow when logging in. A flow is a series of login stages. The home server MUST respond
with all the valid login flows when requested::
The client can login via 3 paths: 1a and 1b, 2a and 2b, or 3. The client should
select one of these paths.
{
"flows": [
{
"type": "<login type1a>",
"stages": [ "<login type 1a>", "<login type 1b>" ]
},
{
"type": "<login type2a>",
"stages": [ "<login type 2a>", "<login type 2b>" ]
},
{
"type": "<login type3>"
}
]
}
After the login is completed, the client's fully-qualified user ID and a new access
token MUST be returned::
{
"user_id": "@user:matrix.org",
"access_token": "abcdef0123456789"
}
The ``user_id`` key is particularly useful if the home server wishes to support
localpart entry of usernames (e.g. "user" rather than "@user:matrix.org"), as the
client may not be able to determine its ``user_id`` in this case.
If a login has multiple requests, the home server may wish to create a session. If
a home server responds with a 'session' key to a request, clients MUST submit it in
subsequent requests until the login is completed::
{
"session": "<session id>"
}
This specification defines the following login types:
- ``m.login.password``
- ``m.login.oauth2``
- ``m.login.email.code``
- ``m.login.email.url``
Password-based
--------------
:Type:
m.login.password
:Description:
Login is supported via a username and password.
To respond to this type, reply with::
{
"type": "m.login.password",
"user": "<user_id or user localpart>",
"password": "<password>"
}
The home server MUST respond with either new credentials, the next stage of the login
process, or a standard error response.
OAuth2-based
------------
:Type:
m.login.oauth2
:Description:
Login is supported via OAuth2 URLs. This login consists of multiple requests.
To respond to this type, reply with::
{
"type": "m.login.oauth2",
"user": "<user_id or user localpart>"
}
The server MUST respond with::
{
"uri": <Authorization Request URI OR service selection URI>
}
The home server acts as a 'confidential' client for the purposes of OAuth2.
If the uri is a ``sevice selection URI``, it MUST point to a webpage which prompts the
user to choose which service to authorize with. On selection of a service, this
MUST link through to an ``Authorization Request URI``. If there is only 1 service which the
home server accepts when logging in, this indirection can be skipped and the
"uri" key can be the ``Authorization Request URI``.
The client then visits the ``Authorization Request URI``, which then shows the OAuth2
Allow/Deny prompt. Hitting 'Allow' returns the ``redirect URI`` with the auth code.
Home servers can choose any path for the ``redirect URI``. The client should visit
the ``redirect URI``, which will then finish the OAuth2 login process, granting the
home server an access token for the chosen service. When the home server gets
this access token, it verifies that the cilent has authorised with the 3rd party, and
can now complete the login. The OAuth2 ``redirect URI`` (with auth code) MUST respond
with either new credentials, the next stage of the login process, or a standard error
response.
For example, if a home server accepts OAuth2 from Google, it would return the
Authorization Request URI for Google::
{
"uri": "https://accounts.google.com/o/oauth2/auth?response_type=code&
client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos"
}
The client then visits this URI and authorizes the home server. The client then
visits the REDIRECT_URI with the auth code= query parameter which returns::
{
"user_id": "@user:matrix.org",
"access_token": "0123456789abcdef"
}
Email-based (code)
------------------
:Type:
m.login.email.code
:Description:
Login is supported by typing in a code which is sent in an email. This login
consists of multiple requests.
To respond to this type, reply with::
{
"type": "m.login.email.code",
"user": "<user_id or user localpart>",
"email": "<email address>"
}
After validating the email address, the home server MUST send an email containing
an authentication code and return::
{
"type": "m.login.email.code",
"session": "<session id>"
}
The second request in this login stage involves sending this authentication code::
{
"type": "m.login.email.code",
"session": "<session id>",
"code": "<code in email sent>"
}
The home server MUST respond to this with either new credentials, the next stage of
the login process, or a standard error response.
Email-based (url)
-----------------
:Type:
m.login.email.url
:Description:
Login is supported by clicking on a URL in an email. This login consists of
multiple requests.
To respond to this type, reply with::
{
"type": "m.login.email.url",
"user": "<user_id or user localpart>",
"email": "<email address>"
}
After validating the email address, the home server MUST send an email containing
an authentication URL and return::
{
"type": "m.login.email.url",
"session": "<session id>"
}
The email contains a URL which must be clicked. After it has been clicked, the
client should perform another request::
{
"type": "m.login.email.url",
"session": "<session id>"
}
The home server MUST respond to this with either new credentials, the next stage of
the login process, or a standard error response.
A common client implementation will be to periodically poll until the link is clicked.
If the link has not been visited yet, a standard error response with an errcode of
``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned.
N-Factor Authentication
-----------------------
Multiple login stages can be combined to create N-factor authentication during login.
This can be achieved by responding with the ``next`` login type on completion of a
previous login stage::
{
"next": "<next login type>"
}
If a home server implements N-factor authentication, it MUST respond with all
``stages`` when initially queried for their login requirements::
{
"type": "<1st login type>",
"stages": [ <1st login type>, <2nd login type>, ... , <Nth login type> ]
}
This can be represented conceptually as::
_______________________
| Login Stage 1 |
| type: "<login type1>" |
| ___________________ |
| |_Request_1_________| | <-- Returns "session" key which is used throughout.
| ___________________ |
| |_Request_2_________| | <-- Returns a "next" value of "login type2"
|_______________________|
|
|
_________V_____________
| Login Stage 2 |
| type: "<login type2>" |
| ___________________ |
| |_Request_1_________| |
| ___________________ |
| |_Request_2_________| |
| ___________________ |
| |_Request_3_________| | <-- Returns a "next" value of "login type3"
|_______________________|
|
|
_________V_____________
| Login Stage 3 |
| type: "<login type3>" |
| ___________________ |
| |_Request_1_________| | <-- Returns user credentials
|_______________________|
Fallback
--------
Clients cannot be expected to be able to know how to process every single
login type. If a client determines it does not know how to handle a given
login type, it should request a login fallback page::
GET matrix/client/api/v1/login/fallback
This MUST return an HTML page which can perform the entire login process.
Identity
========
TODO : Dave
- 3PIDs and identity server, functions
Federation
==========
Federation is the term used to describe how to communicate between Matrix home
servers. Federation is a mechanism by which two home servers can exchange
Matrix event messages, both as a real-time push of current events, and as a
historic fetching mechanism to synchronise past history for clients to view. It
uses HTTP connections between each pair of servers involved as the underlying
transport. Messages are exchanged between servers in real-time by active pushing
from each server's HTTP client into the server of the other. Queries to fetch
historic data for the purpose of back-filling scrollback buffers and the like
can also be performed.
There are three main kinds of communication that occur between home servers:
:Queries:
These are single request/response interactions between a given pair of
servers, initiated by one side sending an HTTP GET request to obtain some
information, and responded by the other. They are not persisted and contain
no long-term significant history. They simply request a snapshot state at the
instant the query is made.
:Ephemeral Data Units (EDUs):
These are notifications of events that are pushed from one home server to
another. They are not persisted and contain no long-term significant history,
nor does the receiving home server have to reply to them.
:Persisted Data Units (PDUs):
These are notifications of events that are broadcast from one home server to
any others that are interested in the same "context" (namely, a Room ID).
They are persisted to long-term storage and form the record of history for
that context.
EDUs and PDUs are further wrapped in an envelope called a Transaction, which is
transferred from the origin to the destination home server using an HTTP PUT request.
Transactions
------------
The transfer of EDUs and PDUs between home servers is performed by an exchange
of Transaction messages, which are encoded as JSON objects, passed over an
HTTP PUT request. A Transaction is meaningful only to the pair of home servers that
exchanged it; they are not globally-meaningful.
Each transaction has:
- An opaque transaction ID.
- A timestamp (UNIX epoch time in milliseconds) generated by its origin server.
- An origin and destination server name.
- A list of "previous IDs".
- A list of PDUs and EDUs - the actual message payload that the Transaction carries.
::
{
"transaction_id":"916d630ea616342b42e98a3be0b74113",
"ts":1404835423000,
"origin":"red",
"destination":"blue",
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
"pdus":[...],
"edus":[...]
}
The ``prev_ids`` field contains a list of previous transaction IDs that
the ``origin`` server has sent to this ``destination``. Its purpose is to act as a
sequence checking mechanism - the destination server can check whether it has
successfully received that Transaction, or ask for a retransmission if not.
The ``pdus`` field of a transaction is a list, containing zero or more PDUs.[*]
Each PDU is itself a JSON object containing a number of keys, the exact details of
which will vary depending on the type of PDU. Similarly, the ``edus`` field is
another list containing the EDUs. This key may be entirely absent if there are
no EDUs to transfer.
(* Normally the PDU list will be non-empty, but the server should cope with
receiving an "empty" transaction, as this is useful for informing peers of other
transaction IDs they should be aware of. This effectively acts as a push
mechanism to encourage peers to continue to replicate content.)
PDUs and EDUs
-------------
All PDUs have:
- An ID
- A context
- A declaration of their type
- A list of other PDU IDs that have been seen recently on that context (regardless of which origin
sent them)
[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
[origin,ref] pair like the prev_pdus are]]
::
{
"pdu_id":"a4ecee13e2accdadf56c1025af232176",
"context":"#example.green",
"origin":"green",
"ts":1404838188000,
"pdu_type":"m.text",
"prev_pdus":[["blue","99d16afbc857975916f1d73e49e52b65"]],
"content":...
"is_state":false
}
In contrast to Transactions, it is important to note that the ``prev_pdus``
field of a PDU refers to PDUs that any origin server has sent, rather than
previous IDs that this ``origin`` has sent. This list may refer to other PDUs sent
by the same origin as the current one, or other origins.
Because of the distributed nature of participants in a Matrix conversation, it
is impossible to establish a globally-consistent total ordering on the events.
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
has received, a partial ordering can be constructed allowing causallity
relationships to be preserved. A client can then display these messages to the
end-user in some order consistent with their content and ensure that no message
that is semantically in reply of an earlier one is ever displayed before it.
PDUs fall into two main categories: those that deliver Events, and those that
synchronise State. For PDUs that relate to State synchronisation, additional
keys exist to support this:
::
{...,
"is_state":true,
"state_key":TODO
"power_level":TODO
"prev_state_id":TODO
"prev_state_origin":TODO}
[[TODO(paul): At this point we should probably have a long description of how
State management works, with descriptions of clobbering rules, power levels, etc
etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
so on. This part needs refining. And writing in its own document as the details
relate to the server/system as a whole, not specifically to server-server
federation.]]
EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
"previous" IDs. The only mandatory fields for these are the type, origin and
destination home server names, and the actual nested content.
::
{"edu_type":"m.presence",
"origin":"blue",
"destination":"orange",
"content":...}
Backfilling
-----------
- What it is, when is it used, how is it done
SRV Records
-----------
- Why it is needed
Security
========
- rate limiting
- crypto (s-s auth)
- E2E
- Lawful intercept + Key Escrow
TODO Mark
Policy Servers
==============
TODO
Content repository
==================
- thumbnail paths
Address book repository
=======================
- format
Glossary
========
- domain specific words/acronyms with definitions
User ID:
An opaque ID which identifies an end-user, which consists of some opaque
localpart combined with the domain name of their home server.

2
setup.py Normal file → Executable file
View File

@@ -1,3 +1,5 @@
#!/usr/bin/env python
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -16,4 +16,4 @@
""" This is a reference implementation of a synapse home server.
"""
__version__ = "0.0.1"
__version__ = "0.1.0"

View File

@@ -19,8 +19,7 @@ from twisted.internet import defer
from synapse.api.constants import Membership
from synapse.api.errors import AuthError, StoreError, Codes
from synapse.api.events.room import (RoomTopicEvent, RoomMemberEvent,
MessageEvent, FeedbackEvent)
from synapse.api.events.room import RoomMemberEvent
import logging
@@ -34,7 +33,7 @@ class Auth(object):
self.store = hs.get_datastore()
@defer.inlineCallbacks
def check(self, event, raises=False):
def check(self, event, snapshot, raises=False):
""" Checks if this event is correctly authed.
Returns:
@@ -44,15 +43,19 @@ class Auth(object):
be raised only if raises=True.
"""
try:
if event.type in [RoomTopicEvent.TYPE, MessageEvent.TYPE,
FeedbackEvent.TYPE]:
yield self.check_joined_room(event.room_id, event.user_id)
defer.returnValue(True)
elif event.type == RoomMemberEvent.TYPE:
allowed = yield self.is_membership_change_allowed(event)
defer.returnValue(allowed)
if hasattr(event, "room_id"):
if event.type == RoomMemberEvent.TYPE:
allowed = yield self.is_membership_change_allowed(event)
defer.returnValue(allowed)
else:
self._check_joined_room(
member=snapshot.membership_state,
user_id=snapshot.user_id,
room_id=snapshot.room_id,
)
defer.returnValue(True)
else:
raise AuthError(500, "Unknown event type %s" % event.type)
raise AuthError(500, "Unknown event: %s" % event)
except AuthError as e:
logger.info("Event auth check failed on event %s with msg: %s",
event, e.msg)
@@ -67,16 +70,22 @@ class Auth(object):
room_id=room_id,
user_id=user_id
)
if not member or member.membership != Membership.JOIN:
raise AuthError(403, "User %s not in room %s" %
(user_id, room_id))
self._check_joined_room(member, user_id, room_id)
defer.returnValue(member)
except AttributeError:
pass
defer.returnValue(None)
def _check_joined_room(self, member, user_id, room_id):
if not member or member.membership != Membership.JOIN:
raise AuthError(403, "User %s not in room %s (%s)" % (
user_id, room_id, repr(member)
))
@defer.inlineCallbacks
def is_membership_change_allowed(self, event):
target_user_id = event.state_key
# does this room even exist
room = yield self.store.get_room(event.room_id)
if not room:
@@ -94,7 +103,7 @@ class Auth(object):
# get info about the target
try:
target = yield self.store.get_room_member(
user_id=event.target_user_id,
user_id=target_user_id,
room_id=event.room_id)
except:
target = None
@@ -108,12 +117,12 @@ class Auth(object):
raise AuthError(403, "You are not in room %s." % event.room_id)
elif target_in_room: # the target is already in the room.
raise AuthError(403, "%s is already in the room." %
event.target_user_id)
target_user_id)
elif Membership.JOIN == membership:
# Joins are valid iff caller == target and they were:
# invited: They are accepting the invitation
# joined: It's a NOOP
if event.user_id != event.target_user_id:
if event.user_id != target_user_id:
raise AuthError(403, "Cannot force another user to join.")
elif room.is_public:
pass # anyone can join public rooms.
@@ -123,10 +132,10 @@ class Auth(object):
elif Membership.LEAVE == membership:
if not caller_in_room: # trying to leave a room you aren't joined
raise AuthError(403, "You are not in room %s." % event.room_id)
elif event.target_user_id != event.user_id:
elif target_user_id != event.user_id:
# trying to force another user to leave
raise AuthError(403, "Cannot force %s to leave." %
event.target_user_id)
target_user_id)
else:
raise AuthError(500, "Unknown membership %s" % membership)
@@ -161,6 +170,8 @@ class Auth(object):
"""
try:
user_id = yield self.store.get_user_by_token(token=token)
if not user_id:
raise StoreError()
defer.returnValue(self.hs.parse_userid(user_id))
except StoreError:
raise AuthError(403, "Unrecognised access token.",

View File

@@ -23,6 +23,7 @@ class Membership(object):
JOIN = u"join"
KNOCK = u"knock"
LEAVE = u"leave"
LIST = (INVITE, JOIN, KNOCK, LEAVE)
class Feedback(object):
@@ -30,8 +31,8 @@ class Feedback(object):
"""Represents the types of feedback a user can send in response to a
message."""
DELIVERED = u"d"
READ = u"r"
DELIVERED = u"delivered"
READ = u"read"
LIST = (DELIVERED, READ)

View File

@@ -41,11 +41,11 @@ class SynapseEvent(JsonEncodedObject):
"room_id",
"user_id", # sender/initiator
"content", # HTTP body, JSON
"state_key",
]
internal_keys = [
"is_state",
"state_key",
"prev_events",
"prev_state",
"depth",

View File

@@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.api.constants import Feedback, Membership
from synapse.api.errors import SynapseError
from . import SynapseEvent
@@ -59,15 +61,15 @@ class RoomMemberEvent(SynapseEvent):
TYPE = "m.room.member"
valid_keys = SynapseEvent.valid_keys + [
"target_user_id", # target
# target is the state_key
"membership", # action
]
def __init__(self, **kwargs):
if "target_user_id" in kwargs:
kwargs["state_key"] = kwargs["target_user_id"]
if "membership" not in kwargs:
kwargs["membership"] = kwargs.get("content", {}).get("membership")
if not kwargs["membership"] in Membership.LIST:
raise SynapseError(400, "Bad membership value.")
super(RoomMemberEvent, self).__init__(**kwargs)
def get_content_template(self):
@@ -91,24 +93,26 @@ class MessageEvent(SynapseEvent):
class FeedbackEvent(SynapseEvent):
TYPE = "m.room.message.feedback"
valid_keys = SynapseEvent.valid_keys + [
"msg_id", # the message ID being acknowledged
"msg_sender_id", # person who is sending the feedback is 'user_id'
"feedback_type", # the type of feedback (delivery, read, etc)
]
valid_keys = SynapseEvent.valid_keys
def __init__(self, **kwargs):
super(FeedbackEvent, self).__init__(**kwargs)
if not kwargs["content"]["type"] in Feedback.LIST:
raise SynapseError(400, "Bad feedback value.")
def get_content_template(self):
return {}
return {
"type": u"string",
"target_event_id": u"string",
"msg_sender_id": u"string"
}
class InviteJoinEvent(SynapseEvent):
TYPE = "m.room.invite_join"
valid_keys = SynapseEvent.valid_keys + [
"target_user_id",
# target_user_id is the state_key
"target_host",
]

View File

@@ -1,196 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.api.constants import Membership
from synapse.api.events.room import RoomMemberEvent
from synapse.api.streams.event import EventsStreamData
from twisted.internet import defer
from twisted.internet import reactor
import logging
logger = logging.getLogger(__name__)
class Notifier(object):
def __init__(self, hs):
self.store = hs.get_datastore()
self.hs = hs
self.stored_event_listeners = {}
@defer.inlineCallbacks
def on_new_room_event(self, event, store_id):
"""Called when there is a new room event which may potentially be sent
down listening users' event streams.
This function looks for interested *users* who may want to be notified
for this event. This is different to users requesting from the event
stream which looks for interested *events* for this user.
Args:
event (SynapseEvent): The new event, which must have a room_id
store_id (int): The ID of this event after it was stored with the
data store.
'"""
member_list = yield self.store.get_room_members(room_id=event.room_id,
membership="join")
if not member_list:
member_list = []
member_list = [u.user_id for u in member_list]
# invites MUST prod the person being invited, who won't be in the room.
if (event.type == RoomMemberEvent.TYPE and
event.content["membership"] == Membership.INVITE):
member_list.append(event.target_user_id)
# similarly, LEAVEs must be sent to the person leaving
if (event.type == RoomMemberEvent.TYPE and
event.content["membership"] == Membership.LEAVE):
member_list.append(event.target_user_id)
for user_id in member_list:
if user_id in self.stored_event_listeners:
self._notify_and_callback(
user_id=user_id,
event_data=event.get_dict(),
stream_type=EventsStreamData.EVENT_TYPE,
store_id=store_id)
def on_new_user_event(self, user_id, event_data, stream_type, store_id):
if user_id in self.stored_event_listeners:
self._notify_and_callback(
user_id=user_id,
event_data=event_data,
stream_type=stream_type,
store_id=store_id
)
def _notify_and_callback(self, user_id, event_data, stream_type, store_id):
logger.debug(
"Notifying %s of a new event.",
user_id
)
stream_ids = list(self.stored_event_listeners[user_id])
for stream_id in stream_ids:
self._notify_and_callback_stream(user_id, stream_id, event_data,
stream_type, store_id)
if not self.stored_event_listeners[user_id]:
del self.stored_event_listeners[user_id]
def _notify_and_callback_stream(self, user_id, stream_id, event_data,
stream_type, store_id):
event_listener = self.stored_event_listeners[user_id].pop(stream_id)
return_event_object = {
k: event_listener[k] for k in ["start", "chunk", "end"]
}
# work out the new end token
token = event_listener["start"]
end = self._next_token(stream_type, store_id, token)
return_event_object["end"] = end
# add the event to the chunk
chunk = event_listener["chunk"]
chunk.append(event_data)
# callback the defer. We know this can't have been resolved before as
# we always remove the event_listener from the map before resolving.
event_listener["defer"].callback(return_event_object)
def _next_token(self, stream_type, store_id, current_token):
stream_handler = self.hs.get_handlers().event_stream_handler
return stream_handler.get_event_stream_token(
stream_type,
store_id,
current_token
)
def store_events_for(self, user_id=None, stream_id=None, from_tok=None):
"""Store all incoming events for this user. This should be paired with
get_events_for to return chunked data.
Args:
user_id (str): The user to monitor incoming events for.
stream (object): The stream that is receiving events
from_tok (str): The token to monitor incoming events from.
"""
event_listener = {
"start": from_tok,
"chunk": [],
"end": from_tok,
"defer": defer.Deferred(),
}
if user_id not in self.stored_event_listeners:
self.stored_event_listeners[user_id] = {stream_id: event_listener}
else:
self.stored_event_listeners[user_id][stream_id] = event_listener
def purge_events_for(self, user_id=None, stream_id=None):
"""Purges any stored events for this user.
Args:
user_id (str): The user to purge stored events for.
"""
try:
del self.stored_event_listeners[user_id][stream_id]
if not self.stored_event_listeners[user_id]:
del self.stored_event_listeners[user_id]
except KeyError:
pass
def get_events_for(self, user_id=None, stream_id=None, timeout=0):
"""Retrieve stored events for this user, waiting if necessary.
It is advisable to wrap this call in a maybeDeferred.
Args:
user_id (str): The user to get events for.
timeout (int): The time in seconds to wait before giving up.
Returns:
A Deferred or a dict containing the chunk data, depending on if
there was data to return yet. The Deferred callback may be None if
there were no events before the timeout expired.
"""
logger.debug("%s is listening for events.", user_id)
try:
streams = self.stored_event_listeners[user_id][stream_id]["chunk"]
if streams:
logger.debug("%s returning existing chunk.", user_id)
return streams
except KeyError:
return None
reactor.callLater(
(timeout / 1000.0), self._timeout, user_id, stream_id
)
return self.stored_event_listeners[user_id][stream_id]["defer"]
def _timeout(self, user_id, stream_id):
try:
# We remove the event_listener from the map so that we can't
# resolve the deferred twice.
event_listeners = self.stored_event_listeners[user_id]
event_listener = event_listeners.pop(stream_id)
event_listener["defer"].callback(None)
logger.debug("%s event listening timed out.", user_id)
except KeyError:
pass

View File

@@ -1,103 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.api.errors import SynapseError
class PaginationConfig(object):
"""A configuration object which stores pagination parameters."""
def __init__(self, from_tok=None, to_tok=None, direction='f', limit=0):
self.from_tok = from_tok
self.to_tok = to_tok
self.direction = direction
self.limit = limit
@classmethod
def from_request(cls, request, raise_invalid_params=True):
params = {
"from_tok": "END",
"direction": 'f',
}
query_param_mappings = [ # 3-tuple of qp_key, attribute, rules
("from", "from_tok", lambda x: type(x) == str),
("to", "to_tok", lambda x: type(x) == str),
("limit", "limit", lambda x: x.isdigit()),
("dir", "direction", lambda x: x == 'f' or x == 'b'),
]
for qp, attr, is_valid in query_param_mappings:
if qp in request.args:
if is_valid(request.args[qp][0]):
params[attr] = request.args[qp][0]
elif raise_invalid_params:
raise SynapseError(400, "%s parameter is invalid." % qp)
return PaginationConfig(**params)
def __str__(self):
return (
"<PaginationConfig from_tok=%s, to_tok=%s, "
"direction=%s, limit=%s>"
) % (self.from_tok, self.to_tok, self.direction, self.limit)
class PaginationStream(object):
""" An interface for streaming data as chunks. """
TOK_END = "END"
def get_chunk(self, config=None):
""" Return the next chunk in the stream.
Args:
config (PaginationConfig): The config to aid which chunk to get.
Returns:
A dict containing the new start token "start", the new end token
"end" and the data "chunk" as a list.
"""
raise NotImplementedError()
class StreamData(object):
""" An interface for obtaining streaming data from a table. """
def __init__(self, hs):
self.hs = hs
self.store = hs.get_datastore()
def get_rows(self, user_id, from_pkey, to_pkey, limit, direction):
""" Get event stream data between the specified pkeys.
Args:
user_id : The user's ID
from_pkey : The starting pkey.
to_pkey : The end pkey. May be -1 to mean "latest".
limit: The max number of results to return.
Returns:
A tuple containing the list of event stream data and the last pkey.
"""
raise NotImplementedError()
def max_token(self):
""" Get the latest currently-valid token.
Returns:
The latest token."""
raise NotImplementedError()

View File

@@ -1,197 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module contains classes for streaming from the event stream: /events.
"""
from twisted.internet import defer
from synapse.api.errors import EventStreamError
from synapse.api.events import SynapseEvent
from synapse.api.events.room import (
RoomMemberEvent, MessageEvent, FeedbackEvent, RoomTopicEvent
)
from synapse.api.streams import PaginationStream, StreamData
import logging
logger = logging.getLogger(__name__)
class EventsStreamData(StreamData):
EVENT_TYPE = "EventsStream"
def __init__(self, hs, room_id=None, feedback=False):
super(EventsStreamData, self).__init__(hs)
self.room_id = room_id
self.with_feedback = feedback
@defer.inlineCallbacks
def get_rows(self, user_id, from_key, to_key, limit, direction):
data, latest_ver = yield self.store.get_room_events(
user_id=user_id,
from_key=from_key,
to_key=to_key,
limit=limit,
room_id=self.room_id,
with_feedback=self.with_feedback
)
defer.returnValue((data, latest_ver))
@defer.inlineCallbacks
def max_token(self):
val = yield self.store.get_room_events_max_id()
defer.returnValue(val)
class EventStream(PaginationStream):
SEPARATOR = '_'
def __init__(self, user_id, stream_data_list):
super(EventStream, self).__init__()
self.user_id = user_id
self.stream_data = stream_data_list
@defer.inlineCallbacks
def fix_tokens(self, pagination_config):
pagination_config.from_tok = yield self.fix_token(
pagination_config.from_tok)
pagination_config.to_tok = yield self.fix_token(
pagination_config.to_tok)
if (
not pagination_config.to_tok
and pagination_config.direction == 'f'
):
pagination_config.to_tok = yield self.get_current_max_token()
logger.debug("pagination_config: %s", pagination_config)
defer.returnValue(pagination_config)
@defer.inlineCallbacks
def fix_token(self, token):
"""Fixes unknown values in a token to known values.
Args:
token (str): The token to fix up.
Returns:
The fixed-up token, which may == token.
"""
if token == PaginationStream.TOK_END:
new_token = yield self.get_current_max_token()
logger.debug("fix_token: From %s to %s", token, new_token)
token = new_token
defer.returnValue(token)
@defer.inlineCallbacks
def get_current_max_token(self):
new_token_parts = []
for s in self.stream_data:
mx = yield s.max_token()
new_token_parts.append(str(mx))
new_token = EventStream.SEPARATOR.join(new_token_parts)
logger.debug("get_current_max_token: %s", new_token)
defer.returnValue(new_token)
@defer.inlineCallbacks
def get_chunk(self, config):
# no support for limit on >1 streams, makes no sense.
if config.limit and len(self.stream_data) > 1:
raise EventStreamError(
400, "Limit not supported on multiplexed streams."
)
chunk_data, next_tok = yield self._get_chunk_data(
config.from_tok,
config.to_tok,
config.limit,
config.direction,
)
defer.returnValue({
"chunk": chunk_data,
"start": config.from_tok,
"end": next_tok
})
@defer.inlineCallbacks
def _get_chunk_data(self, from_tok, to_tok, limit, direction):
""" Get event data between the two tokens.
Tokens are SEPARATOR separated values representing pkey values of
certain tables, and the position determines the StreamData invoked
according to the STREAM_DATA list.
The magic value '-1' can be used to get the latest value.
Args:
from_tok - The token to start from.
to_tok - The token to end at. Must have values > from_tok or be -1.
Returns:
A list of event data.
Raises:
EventStreamError if something went wrong.
"""
# sanity check
if to_tok is not None:
if (from_tok.count(EventStream.SEPARATOR) !=
to_tok.count(EventStream.SEPARATOR) or
(from_tok.count(EventStream.SEPARATOR) + 1) !=
len(self.stream_data)):
raise EventStreamError(400, "Token lengths don't match.")
chunk = []
next_ver = []
for i, (from_pkey, to_pkey) in enumerate(zip(
self._split_token(from_tok),
self._split_token(to_tok)
)):
if from_pkey == to_pkey:
# tokens are the same, we have nothing to do.
next_ver.append(str(to_pkey))
continue
(event_chunk, max_pkey) = yield self.stream_data[i].get_rows(
self.user_id, from_pkey, to_pkey, limit, direction,
)
chunk.extend([
e.get_dict() if isinstance(e, SynapseEvent) else e
for e in event_chunk
])
next_ver.append(str(max_pkey))
defer.returnValue((chunk, EventStream.SEPARATOR.join(next_ver)))
def _split_token(self, token):
"""Splits the given token into a list of pkeys.
Args:
token (str): The token with SEPARATOR values.
Returns:
A list of ints.
"""
if token:
segments = token.split(EventStream.SEPARATOR)
else:
segments = [None] * len(self.stream_data)
return segments

View File

@@ -31,16 +31,34 @@ from synapse.api.urls import (
)
from daemonize import Daemonize
import twisted.manhole.telnet
import argparse
import logging
import logging.config
import sqlite3
import os
import re
logger = logging.getLogger(__name__)
SCHEMAS = [
"transactions",
"pdu",
"users",
"profiles",
"presence",
"im",
"room_aliases",
]
# Remember to update this number every time an incompatible change is made to
# database schema files, so the users will be informed on server restarts.
SCHEMA_VERSION = 1
class SynapseHomeServer(HomeServer):
def build_http_client(self):
@@ -63,31 +81,39 @@ class SynapseHomeServer(HomeServer):
don't have to worry about overwriting existing content.
"""
logging.info("Preparing database: %s...", self.db_name)
with sqlite3.connect(self.db_name) as db_conn:
c = db_conn.cursor()
c.execute("PRAGMA user_version")
row = c.fetchone()
if row and row[0]:
user_version = row[0]
if user_version < SCHEMA_VERSION:
# TODO(paul): add some kind of intelligent fixup here
raise ValueError("Cannot use this database as the " +
"schema version (%d) does not match (%d)" %
(user_version, SCHEMA_VERSION)
)
else:
for sql_loc in SCHEMAS:
sql_script = read_schema(sql_loc)
c.executescript(sql_script)
db_conn.commit()
c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
c.close()
logging.info("Database prepared in %s.", self.db_name)
pool = adbapi.ConnectionPool(
'sqlite3', self.db_name, check_same_thread=False,
cp_min=1, cp_max=1)
schemas = [
"transactions",
"pdu",
"users",
"profiles",
"presence",
"im",
"room_aliases",
]
for sql_loc in schemas:
sql_script = read_schema(sql_loc)
with sqlite3.connect(self.db_name) as db_conn:
c = db_conn.cursor()
c.executescript(sql_script)
c.close()
db_conn.commit()
logging.info("Database prepared in %s.", self.db_name)
return pool
def create_resource_tree(self, web_client, redirect_root_to_web_client):
@@ -182,6 +208,7 @@ class SynapseHomeServer(HomeServer):
def start_listening(self, port):
reactor.listenTCP(port, Site(self.root_resource))
logger.info("Synapse now listening on port %d", port)
def setup_logging(verbosity=0, filename=None, config_path=None):
@@ -237,6 +264,8 @@ def setup():
default="hs.pid")
parser.add_argument("-W", "--webclient", dest="webclient", default=True,
action="store_false", help="Don't host a web client.")
parser.add_argument("--manhole", dest="manhole", type=int, default=None,
help="Turn on the twisted telnet manhole service.")
args = parser.parse_args()
verbosity = int(args.verbose) if args.verbose else None
@@ -255,16 +284,18 @@ def setup():
logger.info("Server hostname: %s", args.host)
if re.search(":[0-9]+$", args.host):
domain_with_port = args.host
else:
domain_with_port = "%s:%s" % (args.host, args.port)
hs = SynapseHomeServer(
args.host,
domain_with_port=domain_with_port,
upload_dir=os.path.abspath("uploads"),
db_name=db_name,
)
# This object doesn't need to be saved because it's set as the handler for
# the replication layer
hs.get_federation()
hs.register_servlets()
hs.create_resource_tree(
@@ -272,7 +303,14 @@ def setup():
redirect_root_to_web_client=True)
hs.start_listening(args.port)
hs.build_db_pool()
hs.get_db_pool()
if args.manhole:
f = twisted.manhole.telnet.ShellFactory()
f.username = "matrix"
f.password = "rabbithole"
f.namespace['hs'] = hs
reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
if args.daemonize:
daemon = Daemonize(

View File

@@ -1,157 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.internet import defer
from .pdu_codec import PduCodec
from synapse.api.errors import AuthError
from synapse.util.logutils import log_function
import logging
logger = logging.getLogger(__name__)
class FederationEventHandler(object):
""" Responsible for:
a) handling received Pdus before handing them on as Events to the rest
of the home server (including auth and state conflict resoultion)
b) converting events that were produced by local clients that may need
to be sent to remote home servers.
"""
def __init__(self, hs):
self.store = hs.get_datastore()
self.replication_layer = hs.get_replication_layer()
self.state_handler = hs.get_state_handler()
# self.auth_handler = gs.get_auth_handler()
self.event_handler = hs.get_handlers().federation_handler
self.server_name = hs.hostname
self.lock_manager = hs.get_room_lock_manager()
self.replication_layer.set_handler(self)
self.pdu_codec = PduCodec(hs)
@log_function
@defer.inlineCallbacks
def handle_new_event(self, event):
""" Takes in an event from the client to server side, that has already
been authed and handled by the state module, and sends it to any
remote home servers that may be interested.
Args:
event
Returns:
Deferred: Resolved when it has successfully been queued for
processing.
"""
yield self.fill_out_prev_events(event)
pdu = self.pdu_codec.pdu_from_event(event)
if not hasattr(pdu, "destinations") or not pdu.destinations:
pdu.destinations = []
yield self.replication_layer.send_pdu(pdu)
@log_function
@defer.inlineCallbacks
def backfill(self, dest, room_id, limit):
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
if not pdus:
defer.returnValue([])
events = [
self.pdu_codec.event_from_pdu(pdu)
for pdu in pdus
]
defer.returnValue(events)
@log_function
def get_state_for_room(self, destination, room_id):
return self.replication_layer.get_state_for_context(
destination, room_id
)
@log_function
@defer.inlineCallbacks
def on_receive_pdu(self, pdu, backfilled):
""" Called by the ReplicationLayer when we have a new pdu. We need to
do auth checks and put it throught the StateHandler.
"""
event = self.pdu_codec.event_from_pdu(pdu)
try:
with (yield self.lock_manager.lock(pdu.context)):
if event.is_state and not backfilled:
is_new_state = yield self.state_handler.handle_new_state(
pdu
)
if not is_new_state:
return
else:
is_new_state = False
yield self.event_handler.on_receive(event, is_new_state, backfilled)
except AuthError:
# TODO: Implement something in federation that allows us to
# respond to PDU.
raise
return
@defer.inlineCallbacks
def _on_new_state(self, pdu, new_state_event):
# TODO: Do any store stuff here. Notifiy C2S about this new
# state.
yield self.store.update_current_state(
pdu_id=pdu.pdu_id,
origin=pdu.origin,
context=pdu.context,
pdu_type=pdu.pdu_type,
state_key=pdu.state_key
)
yield self.event_handler.on_receive(new_state_event)
@defer.inlineCallbacks
def fill_out_prev_events(self, event):
if hasattr(event, "prev_events"):
return
results = yield self.store.get_latest_pdus_in_context(
event.room_id
)
es = [
"%s@%s" % (p_id, origin) for p_id, origin, _ in results
]
event.prev_events = [e for e in es if e != event.event_id]
if results:
event.depth = max([int(v) for _, _, v in results]) + 1
else:
event.depth = 0

View File

@@ -25,7 +25,6 @@ from .units import Pdu
from synapse.util.logutils import log_function
import copy
import json
import logging
@@ -40,28 +39,6 @@ class PduActions(object):
def __init__(self, datastore):
self.store = datastore
@log_function
def persist_received(self, pdu):
""" Persists the given `Pdu` that was received from a remote home
server.
Returns:
Deferred
"""
return self._persist(pdu)
@defer.inlineCallbacks
@log_function
def persist_outgoing(self, pdu):
""" Persists the given `Pdu` that this home server created.
Returns:
Deferred
"""
ret = yield self._persist(pdu)
defer.returnValue(ret)
@log_function
def mark_as_processed(self, pdu):
""" Persist the fact that we have fully processed the given `Pdu`
@@ -71,25 +48,6 @@ class PduActions(object):
"""
return self.store.mark_pdu_as_processed(pdu.pdu_id, pdu.origin)
@defer.inlineCallbacks
@log_function
def populate_previous_pdus(self, pdu):
""" Given an outgoing `Pdu` fill out its `prev_ids` key with the `Pdu`s
that we have received.
Returns:
Deferred
"""
results = yield self.store.get_latest_pdus_in_context(pdu.context)
pdu.prev_pdus = [(p_id, origin) for p_id, origin, _ in results]
vs = [int(v) for _, _, v in results]
if vs:
pdu.depth = max(vs) + 1
else:
pdu.depth = 0
@defer.inlineCallbacks
@log_function
def after_transaction(self, transaction_id, destination, origin):
@@ -143,28 +101,6 @@ class PduActions(object):
depth=pdu.depth
)
@defer.inlineCallbacks
@log_function
def _persist(self, pdu):
kwargs = copy.copy(pdu.__dict__)
unrec_keys = copy.copy(pdu.unrecognized_keys)
del kwargs["content"]
kwargs["content_json"] = json.dumps(pdu.content)
kwargs["unrecognized_keys"] = json.dumps(unrec_keys)
logger.debug("Persisting: %s", repr(kwargs))
if pdu.is_state:
ret = yield self.store.persist_state(**kwargs)
else:
ret = yield self.store.persist_pdu(**kwargs)
yield self.store.update_min_depth_for_context(
pdu.context, pdu.depth
)
defer.returnValue(ret)
class TransactionActions(object):
""" Defines persistence actions that relate to handling Transactions.

View File

@@ -134,10 +134,8 @@ class ReplicationLayer(object):
logger.debug("[%s] Persisting PDU", pdu.pdu_id)
#yield self.pdu_actions.populate_previous_pdus(pdu)
# Save *before* trying to send
yield self.pdu_actions.persist_outgoing(pdu)
yield self.store.persist_event(pdu=pdu)
logger.debug("[%s] Persisted PDU", pdu.pdu_id)
logger.debug("[%s] transaction_layer.enqueue_pdu... ", pdu.pdu_id)
@@ -450,7 +448,7 @@ class ReplicationLayer(object):
logger.exception("Failed to get PDU")
# Persist the Pdu, but don't mark it as processed yet.
yield self.pdu_actions.persist_received(pdu)
yield self.store.persist_event(pdu=pdu)
if not backfilled:
ret = yield self.handler.on_receive_pdu(pdu, backfilled=backfilled)
@@ -509,10 +507,10 @@ class _TransactionQueue(object):
# a transaction in progress. If we do, stick it in the pending_pdus
# table and we'll get back to it later.
destinations = [
destinations = set([
d for d in pdu.destinations
if d != self.server_name
]
])
logger.debug("Sending to: %s", str(destinations))
@@ -543,7 +541,10 @@ class _TransactionQueue(object):
)
def eb(failure):
deferred.errback(failure)
if not deferred.called:
deferred.errback(failure)
else:
logger.exception("Failed to send edu", failure)
self._attempt_new_transaction(destination).addErrback(eb)
return deferred

View File

@@ -15,14 +15,16 @@
from .register import RegistrationHandler
from .room import (
MessageHandler, RoomCreationHandler, RoomMemberHandler, RoomListHandler
RoomCreationHandler, RoomMemberHandler, RoomListHandler
)
from .events import EventStreamHandler
from .message import MessageHandler
from .events import EventStreamHandler, EventHandler
from .federation import FederationHandler
from .login import LoginHandler
from .profile import ProfileHandler
from .presence import PresenceHandler
from .directory import DirectoryHandler
from .typing import TypingNotificationHandler
class Handlers(object):
@@ -39,9 +41,11 @@ class Handlers(object):
self.room_creation_handler = RoomCreationHandler(hs)
self.room_member_handler = RoomMemberHandler(hs)
self.event_stream_handler = EventStreamHandler(hs)
self.event_handler = EventHandler(hs)
self.federation_handler = FederationHandler(hs)
self.profile_handler = ProfileHandler(hs)
self.presence_handler = PresenceHandler(hs)
self.room_list_handler = RoomListHandler(hs)
self.login_handler = LoginHandler(hs)
self.directory_handler = DirectoryHandler(hs)
self.typing_notification_handler = TypingNotificationHandler(hs)

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.internet import defer
class BaseHandler(object):
@@ -26,3 +26,25 @@ class BaseHandler(object):
self.state_handler = hs.get_state_handler()
self.distributor = hs.get_distributor()
self.hs = hs
class BaseRoomHandler(BaseHandler):
@defer.inlineCallbacks
def _on_new_room_event(self, event, snapshot, extra_destinations=[],
extra_users=[]):
snapshot.fill_out_prev_events(event)
yield self.store.persist_event(event)
destinations = set(extra_destinations)
# Send a PDU to all hosts who have joined the room.
destinations.update((yield self.store.get_joined_hosts_for_room(
event.room_id
)))
event.destinations = list(destinations)
self.notifier.on_new_room_event(event, extra_users=extra_users)
federation_handler = self.hs.get_handlers().federation_handler
yield federation_handler.handle_new_event(event, snapshot)

View File

@@ -15,20 +15,19 @@
from twisted.internet import defer
from synapse.api.events import SynapseEvent
from synapse.util.logutils import log_function
from ._base import BaseHandler
from synapse.api.streams.event import (
EventStream, EventsStreamData
)
from synapse.handlers.presence import PresenceStreamData
import logging
logger = logging.getLogger(__name__)
class EventStreamHandler(BaseHandler):
stream_data_classes = [
EventsStreamData,
PresenceStreamData,
]
def __init__(self, hs):
super(EventStreamHandler, self).__init__(hs)
@@ -43,45 +42,13 @@ class EventStreamHandler(BaseHandler):
self.clock = hs.get_clock()
def get_event_stream_token(self, stream_type, store_id, start_token):
"""Return the next token after this event.
Args:
stream_type (str): The StreamData.EVENT_TYPE
store_id (int): The new storage ID assigned from the data store.
start_token (str): The token the user started with.
Returns:
str: The end token.
"""
for i, stream_cls in enumerate(EventStreamHandler.stream_data_classes):
if stream_cls.EVENT_TYPE == stream_type:
# this is the stream for this event, so replace this part of
# the token
store_ids = start_token.split(EventStream.SEPARATOR)
store_ids[i] = str(store_id)
return EventStream.SEPARATOR.join(store_ids)
raise RuntimeError("Didn't find a stream type %s" % stream_type)
self.notifier = hs.get_notifier()
@defer.inlineCallbacks
@log_function
def get_stream(self, auth_user_id, pagin_config, timeout=0):
"""Gets events as an event stream for this user.
This function looks for interesting *events* for this user. This is
different from the notifier, which looks for interested *users* who may
want to know about a single event.
Args:
auth_user_id (str): The user requesting their event stream.
pagin_config (synapse.api.streams.PaginationConfig): The config to
use when obtaining the stream.
timeout (int): The max time to wait for an incoming event in ms.
Returns:
A pagination stream API dict
"""
auth_user = self.hs.parse_userid(auth_user_id)
stream_id = object()
try:
if auth_user not in self._streams_per_user:
self._streams_per_user[auth_user] = 0
@@ -94,41 +61,30 @@ class EventStreamHandler(BaseHandler):
)
self._streams_per_user[auth_user] += 1
# construct an event stream with the correct data ordering
stream_data_list = []
for stream_class in EventStreamHandler.stream_data_classes:
stream_data_list.append(stream_class(self.hs))
event_stream = EventStream(auth_user_id, stream_data_list)
if pagin_config.from_token is None:
pagin_config.from_token = None
# fix unknown tokens to known tokens
pagin_config = yield event_stream.fix_tokens(pagin_config)
rm_handler = self.hs.get_handlers().room_member_handler
room_ids = yield rm_handler.get_rooms_for_user(auth_user)
# register interest in receiving new events
self.notifier.store_events_for(user_id=auth_user_id,
stream_id=stream_id,
from_tok=pagin_config.from_tok)
events, tokens = yield self.notifier.get_events_for(
auth_user, room_ids, pagin_config, timeout
)
# see if we can grab a chunk now
data_chunk = yield event_stream.get_chunk(config=pagin_config)
chunks = [
e.get_dict() if isinstance(e, SynapseEvent) else e
for e in events
]
# if there are previous events, return those. If not, wait on the
# new events for 'timeout' seconds.
if len(data_chunk["chunk"]) == 0 and timeout != 0:
results = yield defer.maybeDeferred(
self.notifier.get_events_for,
user_id=auth_user_id,
stream_id=stream_id,
timeout=timeout
)
if results:
defer.returnValue(results)
chunk = {
"chunk": chunks,
"start": tokens[0].to_string(),
"end": tokens[1].to_string(),
}
defer.returnValue(chunk)
defer.returnValue(data_chunk)
finally:
# cleanup
self.notifier.purge_events_for(user_id=auth_user_id,
stream_id=stream_id)
self._streams_per_user[auth_user] -= 1
if not self._streams_per_user[auth_user]:
del self._streams_per_user[auth_user]
@@ -136,11 +92,39 @@ class EventStreamHandler(BaseHandler):
# 10 seconds of grace to allow the client to reconnect again
# before we think they're gone
def _later():
logger.debug("_later stopped_user_eventstream %s", auth_user)
self.distributor.fire(
"stopped_user_eventstream", auth_user
)
del self._stop_timer_per_user[auth_user]
logger.debug("Scheduling _later: for %s", auth_user)
self._stop_timer_per_user[auth_user] = (
self.clock.call_later(5, _later)
self.clock.call_later(30, _later)
)
class EventHandler(BaseHandler):
@defer.inlineCallbacks
def get_event(self, user, event_id):
"""Retrieve a single specified event.
Args:
user (synapse.types.UserID): The user requesting the event
event_id (str): The event ID to obtain.
Returns:
dict: An event, or None if there is no event matching this ID.
Raises:
SynapseError if there was a problem retrieving this event, or
AuthError if the user does not have the rights to inspect this
event.
"""
event = yield self.store.get_event(event_id)
if not event:
defer.returnValue(None)
return
yield self.auth.check(event, raises=True)
defer.returnValue(event)

View File

@@ -20,6 +20,7 @@ from ._base import BaseHandler
from synapse.api.events.room import InviteJoinEvent, RoomMemberEvent
from synapse.api.constants import Membership
from synapse.util.logutils import log_function
from synapse.federation.pdu_codec import PduCodec
from twisted.internet import defer
@@ -30,8 +31,14 @@ logger = logging.getLogger(__name__)
class FederationHandler(BaseHandler):
"""Handles events that originated from federation.
Responsible for:
a) handling received Pdus before handing them on as Events to the rest
of the home server (including auth and state conflict resoultion)
b) converting events that were produced by local clients that may need
to be sent to remote home servers.
"""
"""Handles events that originated from federation."""
def __init__(self, hs):
super(FederationHandler, self).__init__(hs)
@@ -42,9 +49,61 @@ class FederationHandler(BaseHandler):
self.waiting_for_join_list = {}
self.store = hs.get_datastore()
self.replication_layer = hs.get_replication_layer()
self.state_handler = hs.get_state_handler()
# self.auth_handler = gs.get_auth_handler()
self.server_name = hs.hostname
self.lock_manager = hs.get_room_lock_manager()
self.replication_layer.set_handler(self)
self.pdu_codec = PduCodec(hs)
@log_function
@defer.inlineCallbacks
def on_receive(self, event, is_new_state, backfilled):
def handle_new_event(self, event, snapshot):
""" Takes in an event from the client to server side, that has already
been authed and handled by the state module, and sends it to any
remote home servers that may be interested.
Args:
event
snapshot (.storage.Snapshot): THe snapshot the event happened after
Returns:
Deferred: Resolved when it has successfully been queued for
processing.
"""
pdu = self.pdu_codec.pdu_from_event(event)
if not hasattr(pdu, "destinations") or not pdu.destinations:
pdu.destinations = []
yield self.replication_layer.send_pdu(pdu)
@log_function
@defer.inlineCallbacks
def on_receive_pdu(self, pdu, backfilled):
""" Called by the ReplicationLayer when we have a new pdu. We need to
do auth checks and put it throught the StateHandler.
"""
event = self.pdu_codec.event_from_pdu(pdu)
with (yield self.lock_manager.lock(pdu.context)):
if event.is_state and not backfilled:
is_new_state = yield self.state_handler.handle_new_state(
pdu
)
if not is_new_state:
return
else:
is_new_state = False
# TODO: Implement something in federation that allows us to
# respond to PDU.
if hasattr(event, "state_key") and not is_new_state:
logger.debug("Ignoring old state.")
return
@@ -65,7 +124,7 @@ class FederationHandler(BaseHandler):
content.update({"membership": Membership.JOIN})
new_event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
target_user_id=event.user_id,
state_key=event.user_id,
room_id=event.room_id,
user_id=event.user_id,
membership=Membership.JOIN,
@@ -74,20 +133,19 @@ class FederationHandler(BaseHandler):
yield self.hs.get_handlers().room_member_handler.change_membership(
new_event,
True
do_auth=True
)
else:
with (yield self.room_lock.lock(event.room_id)):
store_id = yield self.store.persist_event(event, backfilled)
yield self.store.persist_event(event, backfilled)
room = yield self.store.get_room(event.room_id)
if not room:
# Huh, let's try and get the current state
try:
federation = self.hs.get_federation()
yield federation.get_state_for_room(
yield self.replication_layer.get_state_for_context(
event.origin, event.room_id
)
@@ -97,9 +155,9 @@ class FederationHandler(BaseHandler):
if self.hs.hostname in hosts:
try:
yield self.store.store_room(
event.room_id,
"",
is_public=False
room_id=event.room_id,
room_creator_user_id="",
is_public=False,
)
except:
pass
@@ -110,33 +168,32 @@ class FederationHandler(BaseHandler):
)
if not backfilled:
yield self.notifier.on_new_room_event(event, store_id)
yield self.notifier.on_new_room_event(event)
if event.type == RoomMemberEvent.TYPE:
if event.membership == Membership.JOIN:
user = self.hs.parse_userid(event.target_user_id)
user = self.hs.parse_userid(event.state_key)
self.distributor.fire(
"user_joined_room", user=user, room_id=event.room_id
)
@log_function
@defer.inlineCallbacks
def backfill(self, dest, room_id, limit):
events = yield self.hs.get_federation().backfill(dest, room_id, limit)
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
for event in events:
try:
yield self.store.persist_event(event, backfilled=True)
except:
logger.exception("Failed to persist event: %s", event)
events = []
for pdu in pdus:
event = self.pdu_codec.event_from_pdu(pdu)
events.append(event)
yield self.store.persist_event(event, backfilled=True)
defer.returnValue(events)
@log_function
@defer.inlineCallbacks
def do_invite_join(self, target_host, room_id, joinee, content):
federation = self.hs.get_federation()
def do_invite_join(self, target_host, room_id, joinee, content, snapshot):
hosts = yield self.store.get_joined_hosts_for_room(room_id)
if self.hs.hostname in hosts:
@@ -146,7 +203,9 @@ class FederationHandler(BaseHandler):
# First get current state to see if we are already joined.
try:
yield federation.get_state_for_room(target_host, room_id)
yield self.replication_layer.get_state_for_context(
target_host, room_id
)
hosts = yield self.store.get_joined_hosts_for_room(room_id)
if self.hs.hostname in hosts:
@@ -166,7 +225,8 @@ class FederationHandler(BaseHandler):
new_event.destinations = [target_host]
yield federation.handle_new_event(new_event)
snapshot.fill_out_prev_events(new_event)
yield self.handle_new_event(new_event, snapshot)
# TODO (erikj): Time out here.
d = defer.Deferred()
@@ -175,8 +235,8 @@ class FederationHandler(BaseHandler):
try:
yield self.store.store_room(
event.room_id,
"",
room_id=room_id,
room_creator_user_id="",
is_public=False
)
except:

304
synapse/handlers/message.py Normal file
View File

@@ -0,0 +1,304 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.internet import defer
from synapse.api.constants import Membership
from synapse.api.events.room import RoomTopicEvent
from synapse.api.errors import RoomError
from synapse.streams.config import PaginationConfig
from ._base import BaseRoomHandler
import logging
logger = logging.getLogger(__name__)
class MessageHandler(BaseRoomHandler):
def __init__(self, hs):
super(MessageHandler, self).__init__(hs)
self.hs = hs
self.clock = hs.get_clock()
self.event_factory = hs.get_event_factory()
@defer.inlineCallbacks
def get_message(self, msg_id=None, room_id=None, sender_id=None,
user_id=None):
""" Retrieve a message.
Args:
msg_id (str): The message ID to obtain.
room_id (str): The room where the message resides.
sender_id (str): The user ID of the user who sent the message.
user_id (str): The user ID of the user making this request.
Returns:
The message, or None if no message exists.
Raises:
SynapseError if something went wrong.
"""
yield self.auth.check_joined_room(room_id, user_id)
# Pull out the message from the db
# msg = yield self.store.get_message(
# room_id=room_id,
# msg_id=msg_id,
# user_id=sender_id
# )
# TODO (erikj): Once we work out the correct c-s api we need to think on how to do this.
defer.returnValue(None)
@defer.inlineCallbacks
def send_message(self, event=None, suppress_auth=False, stamp_event=True):
""" Send a message.
Args:
event : The message event to store.
suppress_auth (bool) : True to suppress auth for this message. This
is primarily so the home server can inject messages into rooms at
will.
stamp_event (bool) : True to stamp event content with server keys.
Raises:
SynapseError if something went wrong.
"""
if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec())
snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
if not suppress_auth:
yield self.auth.check(event, snapshot, raises=True)
yield self._on_new_room_event(event, snapshot)
@defer.inlineCallbacks
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
feedback=False):
"""Get messages in a room.
Args:
user_id (str): The user requesting messages.
room_id (str): The room they want messages from.
pagin_config (synapse.api.streams.PaginationConfig): The pagination
config rules to apply, if any.
feedback (bool): True to get compressed feedback with the messages
Returns:
dict: Pagination API results
"""
yield self.auth.check_joined_room(room_id, user_id)
data_source = self.hs.get_event_sources().sources["room"]
if not pagin_config.from_token:
pagin_config.from_token = yield self.hs.get_event_sources().get_current_token()
user = self.hs.parse_userid(user_id)
events, next_token = yield data_source.get_pagination_rows(
user, pagin_config, room_id
)
chunk = {
"chunk": [e.get_dict() for e in events],
"start": pagin_config.from_token.to_string(),
"end": next_token.to_string(),
}
defer.returnValue(chunk)
@defer.inlineCallbacks
def store_room_data(self, event=None, stamp_event=True):
""" Stores data for a room.
Args:
event : The room path event
stamp_event (bool) : True to stamp event content with server keys.
Raises:
SynapseError if something went wrong.
"""
snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
yield self.auth.check(event, snapshot, raises=True)
if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec())
yield self.state_handler.handle_new_event(event, snapshot)
yield self._on_new_room_event(event, snapshot)
@defer.inlineCallbacks
def get_room_data(self, user_id=None, room_id=None,
event_type=None, state_key="",
public_room_rules=[],
private_room_rules=["join"]):
""" Get data from a room.
Args:
event : The room path event
public_room_rules : A list of membership states the user can be in,
in order to read this data IN A PUBLIC ROOM. An empty list means
'any state'.
private_room_rules : A list of membership states the user can be
in, in order to read this data IN A PRIVATE ROOM. An empty list
means 'any state'.
Returns:
The path data content.
Raises:
SynapseError if something went wrong.
"""
if event_type == RoomTopicEvent.TYPE:
# anyone invited/joined can read the topic
private_room_rules = ["invite", "join"]
# does this room exist
room = yield self.store.get_room(room_id)
if not room:
raise RoomError(403, "Room does not exist.")
# does this user exist in this room
member = yield self.store.get_room_member(
room_id=room_id,
user_id="" if not user_id else user_id)
member_state = member.membership if member else None
if room.is_public and public_room_rules:
# make sure the user meets public room rules
if member_state not in public_room_rules:
raise RoomError(403, "Member does not meet public room rules.")
elif not room.is_public and private_room_rules:
# make sure the user meets private room rules
if member_state not in private_room_rules:
raise RoomError(
403, "Member does not meet private room rules.")
data = yield self.store.get_current_state(
room_id, event_type, state_key
)
defer.returnValue(data)
@defer.inlineCallbacks
def get_feedback(self, event_id):
# yield self.auth.check_joined_room(room_id, user_id)
# Pull out the feedback from the db
fb = yield self.store.get_feedback(event_id)
if fb:
defer.returnValue(fb)
defer.returnValue(None)
@defer.inlineCallbacks
def send_feedback(self, event, stamp_event=True):
if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec())
snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
yield self.auth.check(event, snapshot, raises=True)
# store message in db
yield self._on_new_room_event(event, snapshot)
@defer.inlineCallbacks
def snapshot_all_rooms(self, user_id=None, pagin_config=None,
feedback=False):
"""Retrieve a snapshot of all rooms the user is invited or has joined.
This snapshot may include messages for all rooms where the user is
joined, depending on the pagination config.
Args:
user_id (str): The ID of the user making the request.
pagin_config (synapse.api.streams.PaginationConfig): The pagination
config used to determine how many messages *PER ROOM* to return.
feedback (bool): True to get feedback along with these messages.
Returns:
A list of dicts with "room_id" and "membership" keys for all rooms
the user is currently invited or joined in on. Rooms where the user
is joined on, may return a "messages" key with messages, depending
on the specified PaginationConfig.
"""
room_list = yield self.store.get_rooms_for_user_where_membership_is(
user_id=user_id,
membership_list=[Membership.INVITE, Membership.JOIN]
)
user = self.hs.parse_userid(user_id)
rooms_ret = []
now_token = yield self.hs.get_event_sources().get_current_token()
presence_stream = self.hs.get_event_sources().sources["presence"]
pagination_config = PaginationConfig(from_token=now_token)
presence, _ = yield presence_stream.get_pagination_rows(
user, pagination_config, None
)
limit = pagin_config.limit
if not limit:
limit = 10
for event in room_list:
d = {
"room_id": event.room_id,
"membership": event.membership,
}
if event.membership == Membership.INVITE:
d["inviter"] = event.user_id
rooms_ret.append(d)
if event.membership != Membership.JOIN:
continue
try:
messages, token = yield self.store.get_recent_events_for_room(
event.room_id,
limit=limit,
end_token=now_token.events_key,
)
start_token = now_token.copy_and_replace("events_key", token[0])
end_token = now_token.copy_and_replace("events_key", token[1])
d["messages"] = {
"chunk": [m.get_dict() for m in messages],
"start": start_token.to_string(),
"end": end_token.to_string(),
}
current_state = yield self.store.get_current_state(
event.room_id
)
d["state"] = [c.get_dict() for c in current_state]
except:
logger.exception("Failed to get snapshot")
ret = {
"rooms": rooms_ret,
"presence": presence,
"end": now_token.to_string()
}
defer.returnValue(ret)

View File

@@ -17,7 +17,8 @@ from twisted.internet import defer
from synapse.api.errors import SynapseError, AuthError
from synapse.api.constants import PresenceState
from synapse.api.streams import StreamData
from synapse.util.logutils import log_function
from ._base import BaseHandler
@@ -143,7 +144,7 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks
def is_presence_visible(self, observer_user, observed_user):
defer.returnValue(True)
return
# return
# FIXME (erikj): This code path absolutely kills the database.
assert(observed_user.is_mine)
@@ -159,12 +160,11 @@ class PresenceHandler(BaseHandler):
if allowed_by_subscription:
defer.returnValue(True)
rm_handler = self.homeserver.get_handlers().room_member_handler
for room_id in (yield rm_handler.get_rooms_for_user(observer_user)):
if observed_user in (yield rm_handler.get_room_members(room_id)):
defer.returnValue(True)
share_room = yield self.store.do_users_share_a_room(
[observer_user, observed_user]
)
defer.returnValue(False)
defer.returnValue(share_room)
@defer.inlineCallbacks
def get_state(self, target_user, auth_user):
@@ -190,8 +190,9 @@ class PresenceHandler(BaseHandler):
defer.returnValue(state)
@defer.inlineCallbacks
@log_function
def set_state(self, target_user, auth_user, state):
return
# return
# TODO (erikj): Turn this back on. Why did we end up sending EDUs
# everywhere?
@@ -247,33 +248,42 @@ class PresenceHandler(BaseHandler):
self.push_presence(user, statuscache=statuscache)
@log_function
def started_user_eventstream(self, user):
# TODO(paul): Use "last online" state
self.set_state(user, user, {"state": PresenceState.ONLINE})
@log_function
def stopped_user_eventstream(self, user):
# TODO(paul): Save current state as "last online" state
self.set_state(user, user, {"state": PresenceState.OFFLINE})
@defer.inlineCallbacks
def user_joined_room(self, user, room_id):
localusers = set()
remotedomains = set()
rm_handler = self.homeserver.get_handlers().room_member_handler
yield rm_handler.fetch_room_distributions_into(room_id,
localusers=localusers, remotedomains=remotedomains,
ignore_user=user)
if user.is_mine:
yield self._send_presence_to_distribution(srcuser=user,
localusers=localusers, remotedomains=remotedomains,
self.push_update_to_local_and_remote(
observed_user=user,
room_ids=[room_id],
statuscache=self._get_or_offline_usercache(user),
)
for srcuser in localusers:
yield self._send_presence(srcuser=srcuser, destuser=user,
statuscache=self._get_or_offline_usercache(srcuser),
else:
self.push_update_to_clients(
observed_user=user,
room_ids=[room_id],
statuscache=self._get_or_offline_usercache(user),
)
# We also want to tell them about current presence of people.
rm_handler = self.homeserver.get_handlers().room_member_handler
curr_users = yield rm_handler.get_room_members(room_id)
for local_user in [c for c in curr_users if c.is_mine]:
self.push_update_to_local_and_remote(
observed_user=local_user,
users_to_push=[user],
statuscache=self._get_or_offline_usercache(local_user),
)
@defer.inlineCallbacks
@@ -384,11 +394,13 @@ class PresenceHandler(BaseHandler):
defer.returnValue(presence)
@defer.inlineCallbacks
@log_function
def start_polling_presence(self, user, target_user=None, state=None):
logger.debug("Start polling for presence from %s", user)
if target_user:
target_users = set([target_user])
room_ids = []
else:
presence = yield self.store.get_presence_list(
user.localpart, accepted=True
@@ -402,23 +414,37 @@ class PresenceHandler(BaseHandler):
rm_handler = self.homeserver.get_handlers().room_member_handler
room_ids = yield rm_handler.get_rooms_for_user(user)
for room_id in room_ids:
for member in (yield rm_handler.get_room_members(room_id)):
target_users.add(member)
if state is None:
state = yield self.store.get_presence_state(user.localpart)
else:
# statuscache = self._get_or_make_usercache(user)
# self._user_cachemap_latest_serial += 1
# statuscache.update(state, self._user_cachemap_latest_serial)
pass
localusers, remoteusers = partitionbool(
target_users,
lambda u: u.is_mine
yield self.push_update_to_local_and_remote(
observed_user=user,
users_to_push=target_users,
room_ids=room_ids,
statuscache=self._get_or_make_usercache(user),
)
for target_user in localusers:
self._start_polling_local(user, target_user)
for target_user in target_users:
if target_user.is_mine:
self._start_polling_local(user, target_user)
# We want to tell the person that just came online
# presence state of people they are interested in?
self.push_update_to_clients(
observed_user=target_user,
users_to_push=[user],
statuscache=self._get_or_offline_usercache(target_user),
)
deferreds = []
remoteusers_by_domain = partition(remoteusers, lambda u: u.domain)
remote_users = [u for u in target_users if not u.is_mine]
remoteusers_by_domain = partition(remote_users, lambda u: u.domain)
# Only poll for people in our get_presence_list
for domain in remoteusers_by_domain:
remoteusers = remoteusers_by_domain[domain]
@@ -440,25 +466,26 @@ class PresenceHandler(BaseHandler):
self._local_pushmap[target_localpart].add(user)
self.push_update_to_clients(
observer_user=user,
observed_user=target_user,
statuscache=self._get_or_offline_usercache(target_user),
)
def _start_polling_remote(self, user, domain, remoteusers):
to_poll = set()
for u in remoteusers:
if u not in self._remote_recvmap:
self._remote_recvmap[u] = set()
to_poll.add(u)
self._remote_recvmap[u].add(user)
if not to_poll:
return defer.succeed(None)
return self.federation.send_edu(
destination=domain,
edu_type="m.presence",
content={"poll": [u.to_string() for u in remoteusers]}
content={"poll": [u.to_string() for u in to_poll]}
)
@log_function
def stop_polling_presence(self, user, target_user=None):
logger.debug("Stop polling for presence from %s", user)
@@ -498,20 +525,28 @@ class PresenceHandler(BaseHandler):
if not self._local_pushmap[localpart]:
del self._local_pushmap[localpart]
@log_function
def _stop_polling_remote(self, user, domain, remoteusers):
to_unpoll = set()
for u in remoteusers:
self._remote_recvmap[u].remove(user)
if not self._remote_recvmap[u]:
del self._remote_recvmap[u]
to_unpoll.add(u)
if not to_unpoll:
return defer.succeed(None)
return self.federation.send_edu(
destination=domain,
edu_type="m.presence",
content={"unpoll": [u.to_string() for u in remoteusers]}
content={"unpoll": [u.to_string() for u in to_unpoll]}
)
@defer.inlineCallbacks
@log_function
def push_presence(self, user, statuscache):
assert(user.is_mine)
@@ -527,53 +562,17 @@ class PresenceHandler(BaseHandler):
rm_handler = self.homeserver.get_handlers().room_member_handler
room_ids = yield rm_handler.get_rooms_for_user(user)
for room_id in room_ids:
yield rm_handler.fetch_room_distributions_into(
room_id, localusers=localusers, remotedomains=remotedomains,
ignore_user=user,
)
if not localusers and not remotedomains:
if not localusers and not room_ids:
defer.returnValue(None)
yield self._send_presence_to_distribution(user,
localusers=localusers, remotedomains=remotedomains,
statuscache=statuscache
yield self.push_update_to_local_and_remote(
observed_user=user,
users_to_push=localusers,
remote_domains=remotedomains,
room_ids=room_ids,
statuscache=statuscache,
)
def _send_presence(self, srcuser, destuser, statuscache):
if destuser.is_mine:
self.push_update_to_clients(
observer_user=destuser,
observed_user=srcuser,
statuscache=statuscache)
return defer.succeed(None)
else:
return self._push_presence_remote(srcuser, destuser.domain,
state=statuscache.get_state()
)
@defer.inlineCallbacks
def _send_presence_to_distribution(self, srcuser, localusers=set(),
remotedomains=set(), statuscache=None):
for u in localusers:
logger.debug(" | push to local user %s", u)
self.push_update_to_clients(
observer_user=u,
observed_user=srcuser,
statuscache=statuscache,
)
deferreds = []
for domain in remotedomains:
logger.debug(" | push to remote domain %s", domain)
deferreds.append(self._push_presence_remote(srcuser, domain,
state=statuscache.get_state())
)
yield defer.DeferredList(deferreds)
@defer.inlineCallbacks
def _push_presence_remote(self, user, destination, state=None):
if state is None:
@@ -589,12 +588,17 @@ class PresenceHandler(BaseHandler):
self.clock.time_msec() - state.pop("mtime")
)
user_state = {
"user_id": user.to_string(),
}
user_state.update(**state)
yield self.federation.send_edu(
destination=destination,
edu_type="m.presence",
content={
"push": [
dict(user_id=user.to_string(), **state),
user_state,
],
}
)
@@ -613,12 +617,7 @@ class PresenceHandler(BaseHandler):
rm_handler = self.homeserver.get_handlers().room_member_handler
room_ids = yield rm_handler.get_rooms_for_user(user)
for room_id in room_ids:
yield rm_handler.fetch_room_distributions_into(
room_id, localusers=observers, ignore_user=user
)
if not observers:
if not observers and not room_ids:
break
state = dict(push)
@@ -634,12 +633,12 @@ class PresenceHandler(BaseHandler):
self._user_cachemap_latest_serial += 1
statuscache.update(state, serial=self._user_cachemap_latest_serial)
for observer_user in observers:
self.push_update_to_clients(
observer_user=observer_user,
observed_user=user,
statuscache=statuscache,
)
self.push_update_to_clients(
observed_user=user,
users_to_push=observers,
room_ids=room_ids,
statuscache=statuscache,
)
if state["state"] == PresenceState.OFFLINE:
del self._user_cachemap[user]
@@ -673,49 +672,54 @@ class PresenceHandler(BaseHandler):
yield defer.DeferredList(deferreds)
def push_update_to_clients(self, observer_user, observed_user,
statuscache):
state = statuscache.make_event(user=observed_user, clock=self.clock)
@defer.inlineCallbacks
def push_update_to_local_and_remote(self, observed_user,
users_to_push=[], room_ids=[],
remote_domains=[],
statuscache=None):
self.notifier.on_new_user_event(
observer_user.to_string(),
event_data=statuscache.make_event(
user=observed_user,
clock=self.clock
),
stream_type=PresenceStreamData,
store_id=statuscache.serial
localusers, remoteusers = partitionbool(
users_to_push,
lambda u: u.is_mine
)
localusers = set(localusers)
class PresenceStreamData(StreamData):
def __init__(self, hs):
super(PresenceStreamData, self).__init__(hs)
self.presence = hs.get_handlers().presence_handler
self.push_update_to_clients(
observed_user=observed_user,
users_to_push=localusers,
room_ids=room_ids,
statuscache=statuscache,
)
def get_rows(self, user_id, from_key, to_key, limit, direction):
from_key = int(from_key)
to_key = int(to_key)
remote_domains = set(remote_domains)
remote_domains |= set([r.domain for r in remoteusers])
for room_id in room_ids:
remote_domains.update(
(yield self.store.get_joined_hosts_for_room(room_id))
)
cachemap = self.presence._user_cachemap
remote_domains.discard(self.hs.hostname)
# TODO(paul): limit, and filter by visibility
updates = [(k, cachemap[k]) for k in cachemap
if from_key < cachemap[k].serial <= to_key]
deferreds = []
for domain in remote_domains:
logger.debug(" | push to remote domain %s", domain)
deferreds.append(
self._push_presence_remote(
observed_user, domain, state=statuscache.get_state()
)
)
if updates:
clock = self.presence.clock
yield defer.DeferredList(deferreds)
latest_serial = max([x[1].serial for x in updates])
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
return ((data, latest_serial))
else:
return (([], self.presence._user_cachemap_latest_serial))
defer.returnValue((localusers, remote_domains))
def max_token(self):
return self.presence._user_cachemap_latest_serial
PresenceStreamData.EVENT_TYPE = PresenceStreamData
def push_update_to_clients(self, observed_user, users_to_push=[],
room_ids=[], statuscache=None):
self.notifier.on_new_user_event(
users_to_push,
room_ids,
)
class UserPresenceCache(object):

View File

@@ -18,301 +18,19 @@ from twisted.internet import defer
from synapse.types import UserID, RoomAlias, RoomID
from synapse.api.constants import Membership
from synapse.api.errors import RoomError, StoreError, SynapseError
from synapse.api.errors import StoreError, SynapseError
from synapse.api.events.room import (
RoomTopicEvent, MessageEvent, InviteJoinEvent, RoomMemberEvent,
RoomConfigEvent
RoomMemberEvent, RoomConfigEvent
)
from synapse.api.streams.event import EventStream, EventsStreamData
from synapse.handlers.presence import PresenceStreamData
from synapse.util import stringutils
from ._base import BaseHandler
from ._base import BaseRoomHandler
import logging
import json
logger = logging.getLogger(__name__)
class MessageHandler(BaseHandler):
def __init__(self, hs):
super(MessageHandler, self).__init__(hs)
self.hs = hs
self.clock = hs.get_clock()
self.event_factory = hs.get_event_factory()
@defer.inlineCallbacks
def get_message(self, msg_id=None, room_id=None, sender_id=None,
user_id=None):
""" Retrieve a message.
Args:
msg_id (str): The message ID to obtain.
room_id (str): The room where the message resides.
sender_id (str): The user ID of the user who sent the message.
user_id (str): The user ID of the user making this request.
Returns:
The message, or None if no message exists.
Raises:
SynapseError if something went wrong.
"""
yield self.auth.check_joined_room(room_id, user_id)
# Pull out the message from the db
# msg = yield self.store.get_message(
# room_id=room_id,
# msg_id=msg_id,
# user_id=sender_id
# )
# TODO (erikj): Once we work out the correct c-s api we need to think on how to do this.
defer.returnValue(None)
@defer.inlineCallbacks
def send_message(self, event=None, suppress_auth=False, stamp_event=True):
""" Send a message.
Args:
event : The message event to store.
suppress_auth (bool) : True to suppress auth for this message. This
is primarily so the home server can inject messages into rooms at
will.
stamp_event (bool) : True to stamp event content with server keys.
Raises:
SynapseError if something went wrong.
"""
if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec())
with (yield self.room_lock.lock(event.room_id)):
if not suppress_auth:
yield self.auth.check(event, raises=True)
# store message in db
store_id = yield self.store.persist_event(event)
event.destinations = yield self.store.get_joined_hosts_for_room(
event.room_id
)
self.notifier.on_new_room_event(event, store_id)
yield self.hs.get_federation().handle_new_event(event)
@defer.inlineCallbacks
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
feedback=False):
"""Get messages in a room.
Args:
user_id (str): The user requesting messages.
room_id (str): The room they want messages from.
pagin_config (synapse.api.streams.PaginationConfig): The pagination
config rules to apply, if any.
feedback (bool): True to get compressed feedback with the messages
Returns:
dict: Pagination API results
"""
yield self.auth.check_joined_room(room_id, user_id)
data_source = [
EventsStreamData(self.hs, room_id=room_id, feedback=feedback)
]
event_stream = EventStream(user_id, data_source)
pagin_config = yield event_stream.fix_tokens(pagin_config)
data_chunk = yield event_stream.get_chunk(config=pagin_config)
defer.returnValue(data_chunk)
@defer.inlineCallbacks
def store_room_data(self, event=None, stamp_event=True):
""" Stores data for a room.
Args:
event : The room path event
stamp_event (bool) : True to stamp event content with server keys.
Raises:
SynapseError if something went wrong.
"""
with (yield self.room_lock.lock(event.room_id)):
yield self.auth.check(event, raises=True)
if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec())
yield self.state_handler.handle_new_event(event)
# store in db
store_id = yield self.store.persist_event(event)
event.destinations = yield self.store.get_joined_hosts_for_room(
event.room_id
)
self.notifier.on_new_room_event(event, store_id)
yield self.hs.get_federation().handle_new_event(event)
@defer.inlineCallbacks
def get_room_data(self, user_id=None, room_id=None,
event_type=None, state_key="",
public_room_rules=[],
private_room_rules=["join"]):
""" Get data from a room.
Args:
event : The room path event
public_room_rules : A list of membership states the user can be in,
in order to read this data IN A PUBLIC ROOM. An empty list means
'any state'.
private_room_rules : A list of membership states the user can be
in, in order to read this data IN A PRIVATE ROOM. An empty list
means 'any state'.
Returns:
The path data content.
Raises:
SynapseError if something went wrong.
"""
if event_type == RoomTopicEvent.TYPE:
# anyone invited/joined can read the topic
private_room_rules = ["invite", "join"]
# does this room exist
room = yield self.store.get_room(room_id)
if not room:
raise RoomError(403, "Room does not exist.")
# does this user exist in this room
member = yield self.store.get_room_member(
room_id=room_id,
user_id="" if not user_id else user_id)
member_state = member.membership if member else None
if room.is_public and public_room_rules:
# make sure the user meets public room rules
if member_state not in public_room_rules:
raise RoomError(403, "Member does not meet public room rules.")
elif not room.is_public and private_room_rules:
# make sure the user meets private room rules
if member_state not in private_room_rules:
raise RoomError(
403, "Member does not meet private room rules.")
data = yield self.store.get_current_state(
room_id, event_type, state_key
)
defer.returnValue(data)
@defer.inlineCallbacks
def get_feedback(self, event_id):
# yield self.auth.check_joined_room(room_id, user_id)
# Pull out the feedback from the db
fb = yield self.store.get_feedback(event_id)
if fb:
defer.returnValue(fb)
defer.returnValue(None)
@defer.inlineCallbacks
def send_feedback(self, event, stamp_event=True):
if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec())
with (yield self.room_lock.lock(event.room_id)):
yield self.auth.check(event, raises=True)
# store message in db
store_id = yield self.store.persist_event(event)
event.destinations = yield self.store.get_joined_hosts_for_room(
event.room_id
)
yield self.hs.get_federation().handle_new_event(event)
self.notifier.on_new_room_event(event, store_id)
@defer.inlineCallbacks
def snapshot_all_rooms(self, user_id=None, pagin_config=None,
feedback=False):
"""Retrieve a snapshot of all rooms the user is invited or has joined.
This snapshot may include messages for all rooms where the user is
joined, depending on the pagination config.
Args:
user_id (str): The ID of the user making the request.
pagin_config (synapse.api.streams.PaginationConfig): The pagination
config used to determine how many messages *PER ROOM* to return.
feedback (bool): True to get feedback along with these messages.
Returns:
A list of dicts with "room_id" and "membership" keys for all rooms
the user is currently invited or joined in on. Rooms where the user
is joined on, may return a "messages" key with messages, depending
on the specified PaginationConfig.
"""
room_list = yield self.store.get_rooms_for_user_where_membership_is(
user_id=user_id,
membership_list=[Membership.INVITE, Membership.JOIN]
)
rooms_ret = []
now_rooms_token = yield self.store.get_room_events_max_id()
# FIXME (erikj): Fix this.
presence_stream = PresenceStreamData(self.hs)
now_presence_token = yield presence_stream.max_token()
presence = yield presence_stream.get_rows(
user_id, 0, now_presence_token, None, None
)
# FIXME (erikj): We need to not generate this token,
now_token = "%s_%s" % (now_rooms_token, now_presence_token)
for event in room_list:
d = {
"room_id": event.room_id,
"membership": event.membership,
}
if event.membership == Membership.INVITE:
d["inviter"] = event.user_id
rooms_ret.append(d)
if event.membership != Membership.JOIN:
continue
try:
messages, token = yield self.store.get_recent_events_for_room(
event.room_id,
limit=10,
end_token=now_rooms_token,
)
d["messages"] = {
"chunk": [m.get_dict() for m in messages],
"start": token[0],
"end": token[1],
}
current_state = yield self.store.get_current_state(event.room_id)
d["state"] = [c.get_dict() for c in current_state]
except:
logger.exception("Failed to get snapshot")
user = self.hs.parse_userid(user_id)
ret = {"rooms": rooms_ret, "presence": presence[0], "end": now_token}
# logger.debug("snapshot_all_rooms returning: %s", ret)
defer.returnValue(ret)
class RoomCreationHandler(BaseHandler):
class RoomCreationHandler(BaseRoomHandler):
@defer.inlineCallbacks
def create_room(self, user_id, room_id, config):
@@ -383,6 +101,13 @@ class RoomCreationHandler(BaseHandler):
content=config,
)
snapshot = yield self.store.snapshot_room(
room_id=room_id,
user_id=user_id,
state_type=RoomConfigEvent.TYPE,
state_key="",
)
if room_alias:
yield self.store.create_room_alias_association(
room_id=room_id,
@@ -390,16 +115,16 @@ class RoomCreationHandler(BaseHandler):
servers=[self.hs.hostname],
)
yield self.state_handler.handle_new_event(config_event)
yield self.state_handler.handle_new_event(config_event, snapshot)
# store_id = persist...
yield self.hs.get_federation().handle_new_event(config_event)
# self.notifier.on_new_room_event(event, store_id)
federation_handler = self.hs.get_handlers().federation_handler
yield federation_handler.handle_new_event(config_event, snapshot)
content = {"membership": Membership.JOIN}
join_event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
target_user_id=user_id,
state_key=user_id,
room_id=room_id,
user_id=user_id,
membership=Membership.JOIN,
@@ -418,7 +143,7 @@ class RoomCreationHandler(BaseHandler):
defer.returnValue(result)
class RoomMemberHandler(BaseHandler):
class RoomMemberHandler(BaseRoomHandler):
# TODO(paul): This handler currently contains a messy conflation of
# low-level API that works on UserID objects and so on, and REST-level
# API that takes ID strings and returns pagination chunks. These concerns
@@ -490,7 +215,7 @@ class RoomMemberHandler(BaseHandler):
for entry in member_list
]
chunk_data = {
"start": "START", # FIXME (erikj): START is no longer a valid value
"start": "START", # FIXME (erikj): START is no longer valid
"end": "END",
"chunk": event_list
}
@@ -527,9 +252,15 @@ class RoomMemberHandler(BaseHandler):
Raises:
SynapseError if there was a problem changing the membership.
"""
target_user_id = event.state_key
snapshot = yield self.store.snapshot_room(
event.room_id, event.user_id,
RoomMemberEvent.TYPE, target_user_id
)
## TODO(markjh): get prev state from snapshot.
prev_state = yield self.store.get_room_member(
event.target_user_id, event.room_id
target_user_id, event.room_id
)
if prev_state:
@@ -548,24 +279,22 @@ class RoomMemberHandler(BaseHandler):
# if this HS is not currently in the room, i.e. we have to do the
# invite/join dance.
if event.membership == Membership.JOIN:
yield self._do_join(event, do_auth=do_auth)
yield self._do_join(event, snapshot, do_auth=do_auth)
else:
# This is not a JOIN, so we can handle it normally.
if do_auth:
yield self.auth.check(event, raises=True)
yield self.auth.check(event, snapshot, raises=True)
prev_state = yield self.store.get_room_member(
event.target_user_id, event.room_id
)
if prev_state and prev_state.membership == event.membership:
# double same action, treat this event as a NOOP.
defer.returnValue({})
return
yield self.state_handler.handle_new_event(event)
yield self.state_handler.handle_new_event(event, snapshot)
yield self._do_local_membership_update(
event,
membership=event.content["membership"],
snapshot=snapshot,
)
defer.returnValue({"room_id": room_id})
@@ -588,20 +317,25 @@ class RoomMemberHandler(BaseHandler):
content.update({"membership": Membership.JOIN})
new_event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
target_user_id=joinee.to_string(),
state_key=joinee.to_string(),
room_id=room_id,
user_id=joinee.to_string(),
membership=Membership.JOIN,
content=content,
)
yield self._do_join(new_event, room_host=host, do_auth=True)
snapshot = yield self.store.snapshot_room(
room_id, joinee.to_string(), RoomMemberEvent.TYPE,
joinee.to_string()
)
yield self._do_join(new_event, snapshot, room_host=host, do_auth=True)
defer.returnValue({"room_id": room_id})
@defer.inlineCallbacks
def _do_join(self, event, room_host=None, do_auth=True):
joinee = self.hs.parse_userid(event.target_user_id)
def _do_join(self, event, snapshot, room_host=None, do_auth=True):
joinee = self.hs.parse_userid(event.state_key)
# room_id = RoomID.from_string(event.room_id, self.hs)
room_id = event.room_id
@@ -622,6 +356,7 @@ class RoomMemberHandler(BaseHandler):
elif room_host:
should_do_dance = True
else:
# TODO(markjh): get prev_state from snapshot
prev_state = yield self.store.get_room_member(
joinee.to_string(), room_id
)
@@ -641,7 +376,7 @@ class RoomMemberHandler(BaseHandler):
if should_do_dance:
handler = self.hs.get_handlers().federation_handler
have_joined = yield handler.do_invite_join(
room_host, room_id, event.user_id, event.content
room_host, room_id, event.user_id, event.content, snapshot
)
# We want to do the _do_update inside the room lock.
@@ -649,12 +384,13 @@ class RoomMemberHandler(BaseHandler):
logger.debug("Doing normal join")
if do_auth:
yield self.auth.check(event, raises=True)
yield self.auth.check(event, snapshot, raises=True)
yield self.state_handler.handle_new_event(event)
yield self.state_handler.handle_new_event(event, snapshot)
yield self._do_local_membership_update(
event,
membership=event.content["membership"],
snapshot=snapshot,
)
user = self.hs.parse_userid(event.user_id)
@@ -698,38 +434,28 @@ class RoomMemberHandler(BaseHandler):
defer.returnValue([r.room_id for r in rooms])
@defer.inlineCallbacks
def _do_local_membership_update(self, event, membership):
# store membership
store_id = yield self.store.persist_event(event)
# Send a PDU to all hosts who have joined the room.
destinations = yield self.store.get_joined_hosts_for_room(
event.room_id
)
def _do_local_membership_update(self, event, membership, snapshot):
destinations = []
# If we're inviting someone, then we should also send it to that
# HS.
target_user_id = event.state_key
target_user = self.hs.parse_userid(target_user_id)
if membership == Membership.INVITE:
host = UserID.from_string(
event.target_user_id, self.hs
).domain
host = target_user.domain
destinations.append(host)
# If we are joining a remote HS, include that.
if membership == Membership.JOIN:
host = UserID.from_string(
event.target_user_id, self.hs
).domain
host = target_user.domain
destinations.append(host)
event.destinations = list(set(destinations))
return self._on_new_room_event(
event, snapshot, extra_destinations=destinations,
extra_users=[target_user]
)
yield self.hs.get_federation().handle_new_event(event)
self.notifier.on_new_room_event(event, store_id)
class RoomListHandler(BaseHandler):
class RoomListHandler(BaseRoomHandler):
@defer.inlineCallbacks
def get_public_room_list(self):

147
synapse/handlers/typing.py Normal file
View File

@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.internet import defer
from ._base import BaseHandler
from synapse.api.errors import SynapseError, AuthError
import logging
from collections import namedtuple
logger = logging.getLogger(__name__)
# A tiny object useful for storing a user's membership in a room, as a mapping
# key
RoomMember = namedtuple("RoomMember", ("room_id", "user"))
class TypingNotificationHandler(BaseHandler):
def __init__(self, hs):
super(TypingNotificationHandler, self).__init__(hs)
self.homeserver = hs
self.clock = hs.get_clock()
self.federation = hs.get_replication_layer()
self.federation.register_edu_handler("m.typing", self._recv_edu)
self._member_typing_until = {}
@defer.inlineCallbacks
def started_typing(self, target_user, auth_user, room_id, timeout):
if not target_user.is_mine:
raise SynapseError(400, "User is not hosted on this Home Server")
if target_user != auth_user:
raise AuthError(400, "Cannot set another user's typing state")
until = self.clock.time_msec() + timeout
member = RoomMember(room_id=room_id, user=target_user)
was_present = member in self._member_typing_until
self._member_typing_until[member] = until
if was_present:
# No point sending another notification
defer.returnValue(None)
yield self._push_update(
room_id=room_id,
user=target_user,
typing=True,
)
@defer.inlineCallbacks
def stopped_typing(self, target_user, auth_user, room_id):
if not target_user.is_mine:
raise SynapseError(400, "User is not hosted on this Home Server")
if target_user != auth_user:
raise AuthError(400, "Cannot set another user's typing state")
member = RoomMember(room_id=room_id, user=target_user)
if member not in self._member_typing_until:
# No point
defer.returnValue(None)
yield self._push_update(
room_id=room_id,
user=target_user,
typing=False,
)
@defer.inlineCallbacks
def _push_update(self, room_id, user, typing):
localusers = set()
remotedomains = set()
rm_handler = self.homeserver.get_handlers().room_member_handler
yield rm_handler.fetch_room_distributions_into(room_id,
localusers=localusers, remotedomains=remotedomains,
ignore_user=user)
for u in localusers:
self.push_update_to_clients(
room_id=room_id,
observer_user=u,
observed_user=user,
typing=typing,
)
deferreds = []
for domain in remotedomains:
deferreds.append(self.federation.send_edu(
destination=domain,
edu_type="m.typing",
content={
"room_id": room_id,
"user_id": user.to_string(),
"typing": typing,
},
))
yield defer.DeferredList(deferreds, consumeErrors=False)
@defer.inlineCallbacks
def _recv_edu(self, origin, content):
room_id = content["room_id"]
user = self.homeserver.parse_userid(content["user_id"])
localusers = set()
rm_handler = self.homeserver.get_handlers().room_member_handler
yield rm_handler.fetch_room_distributions_into(room_id,
localusers=localusers)
for u in localusers:
self.push_update_to_clients(
room_id=room_id,
observer_user=u,
observed_user=user,
typing=content["typing"]
)
def push_update_to_clients(self, room_id, observer_user, observed_user,
typing):
# TODO(paul) steal this from presence.py
pass

View File

@@ -325,7 +325,9 @@ class ContentRepoResource(resource.Resource):
# FIXME (erikj): These should use constants.
file_name = os.path.basename(fname)
url = "http://%s/matrix/content/%s" % (self.hs.hostname, file_name)
url = "http://%s/matrix/content/%s" % (
self.hs.domain_with_port, file_name
)
respond_with_json_bytes(request, 200,
json.dumps({"content_token": url}),

241
synapse/notifier.py Normal file
View File

@@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.internet import defer, reactor
from synapse.util.logutils import log_function
import logging
logger = logging.getLogger(__name__)
class _NotificationListener(object):
""" This represents a single client connection to the events stream.
The events stream handler will have yielded to the deferred, so to
notify the handler it is sufficient to resolve the deferred.
This listener will also keep track of which rooms it is listening in
so that it can remove itself from the indexes in the Notifier class.
"""
def __init__(self, user, rooms, from_token, limit, timeout, deferred):
self.user = user
self.from_token = from_token
self.limit = limit
self.timeout = timeout
self.deferred = deferred
self.rooms = rooms
self.pending_notifications = []
def notify(self, notifier, events, start_token, end_token):
""" Inform whoever is listening about the new events. This will
also remove this listener from all the indexes in the Notifier
it knows about.
"""
result = (events, (start_token, end_token))
try:
self.deferred.callback(result)
except defer.AlreadyCalledError:
pass
for room in self.rooms:
lst = notifier.rooms_to_listeners.get(room, set())
lst.discard(self)
notifier.user_to_listeners.get(self.user, set()).discard(self)
class Notifier(object):
""" This class is responsible for notifying any listeners when there are
new events available for it.
Primarily used from the /events stream.
"""
def __init__(self, hs):
self.hs = hs
self.rooms_to_listeners = {}
self.user_to_listeners = {}
self.event_sources = hs.get_event_sources()
hs.get_distributor().observe(
"user_joined_room", self._user_joined_room
)
@log_function
@defer.inlineCallbacks
def on_new_room_event(self, event, extra_users=[]):
""" Used by handlers to inform the notifier something has happened
in the room, room event wise.
This triggers the notifier to wake up any listeners that are
listening to the room, and any listeners for the users in the
`extra_users` param.
"""
room_id = event.room_id
source = self.event_sources.sources["room"]
listeners = self.rooms_to_listeners.get(room_id, set()).copy()
for user in extra_users:
listeners |= self.user_to_listeners.get(user, set()).copy()
logger.debug("on_new_room_event listeners %s", listeners)
# TODO (erikj): Can we make this more efficient by hitting the
# db once?
for listener in listeners:
events, end_token = yield source.get_new_events_for_user(
listener.user,
listener.from_token,
listener.limit,
)
if events:
listener.notify(
self, events, listener.from_token, end_token
)
@defer.inlineCallbacks
@log_function
def on_new_user_event(self, users=[], rooms=[]):
""" Used to inform listeners that something has happend
presence/user event wise.
Will wake up all listeners for the given users and rooms.
"""
source = self.event_sources.sources["presence"]
listeners = set()
for user in users:
listeners |= self.user_to_listeners.get(user, set()).copy()
for room in rooms:
listeners |= self.rooms_to_listeners.get(room, set()).copy()
for listener in listeners:
events, end_token = yield source.get_new_events_for_user(
listener.user,
listener.from_token,
listener.limit,
)
if events:
listener.notify(
self, events, listener.from_token, end_token
)
def get_events_for(self, user, rooms, pagination_config, timeout):
""" For the given user and rooms, return any new events for them. If
there are no new events wait for up to `timeout` milliseconds for any
new events to happen before returning.
"""
deferred = defer.Deferred()
self._get_events(
deferred, user, rooms, pagination_config.from_token,
pagination_config.limit, timeout
).addErrback(deferred.errback)
return deferred
@defer.inlineCallbacks
def _get_events(self, deferred, user, rooms, from_token, limit, timeout):
if not from_token:
from_token = yield self.event_sources.get_current_token()
listener = _NotificationListener(
user,
rooms,
from_token,
limit,
timeout,
deferred,
)
if timeout:
reactor.callLater(timeout/1000, self._timeout_listener, listener)
self._register_with_keys(listener)
yield self._check_for_updates(listener)
if not timeout:
self._timeout_listener(listener)
return
def _timeout_listener(self, listener):
# TODO (erikj): We should probably set to_token to the current max
# rather than reusing from_token.
listener.notify(
self,
[],
listener.from_token,
listener.from_token,
)
@log_function
def _register_with_keys(self, listener):
for room in listener.rooms:
s = self.rooms_to_listeners.setdefault(room, set())
s.add(listener)
self.user_to_listeners.setdefault(listener.user, set()).add(listener)
@defer.inlineCallbacks
@log_function
def _check_for_updates(self, listener):
# TODO (erikj): We need to think about limits across multiple sources
events = []
from_token = listener.from_token
limit = listener.limit
# TODO (erikj): DeferredList?
for source in self.event_sources.sources.values():
stuff, new_token = yield source.get_new_events_for_user(
listener.user,
from_token,
limit,
)
events.extend(stuff)
from_token = new_token
end_token = from_token
if events:
listener.notify(self, events, listener.from_token, end_token)
defer.returnValue(listener)
def _user_joined_room(self, user, room_id):
new_listeners = self.user_to_listeners.get(user, set())
listeners = self.rooms_to_listeners.setdefault(room_id, set())
listeners |= new_listeners

View File

@@ -15,7 +15,7 @@
from . import (
room, events, register, login, profile, public, presence, im, directory
room, events, register, login, profile, presence, initial_sync, directory
)
@@ -39,7 +39,6 @@ class RestServletFactory(object):
register.register_servlets(hs, client_resource)
login.register_servlets(hs, client_resource)
profile.register_servlets(hs, client_resource)
public.register_servlets(hs, client_resource)
presence.register_servlets(hs, client_resource)
im.register_servlets(hs, client_resource)
initial_sync.register_servlets(hs, client_resource)
directory.register_servlets(hs, client_resource)

View File

@@ -15,6 +15,7 @@
""" 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
@@ -59,6 +60,7 @@ class RestServlet(object):
self.handlers = hs.get_handlers()
self.event_factory = hs.get_event_factory()
self.auth = hs.get_auth()
self.txns = HttpTransactionStore()
def register(self, http_server):
""" Register this servlet with the given HTTP server. """

View File

@@ -31,7 +31,7 @@ def register_servlets(hs, http_server):
class ClientDirectoryServer(RestServlet):
PATTERN = client_path_pattern("/ds/room/(?P<room_alias>[^/]*)$")
PATTERN = client_path_pattern("/directory/room/(?P<room_alias>[^/]*)$")
@defer.inlineCallbacks
def on_GET(self, request, room_alias):

View File

@@ -17,7 +17,7 @@
from twisted.internet import defer
from synapse.api.errors import SynapseError
from synapse.api.streams import PaginationConfig
from synapse.streams.config import PaginationConfig
from synapse.rest.base import RestServlet, client_path_pattern
@@ -41,11 +41,29 @@ class EventStreamRestServlet(RestServlet):
chunk = yield handler.get_stream(auth_user.to_string(), pagin_config,
timeout=timeout)
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<event_id>[^/]*)$")
@defer.inlineCallbacks
def on_GET(self, request, event_id):
auth_user = yield self.auth.get_user_by_req(request)
handler = self.handlers.event_handler
event = yield handler.get_event(auth_user, event_id)
if event:
defer.returnValue((200, event.get_dict()))
else:
defer.returnValue((404, "Event not found."))
def register_servlets(hs, http_server):
EventStreamRestServlet(hs).register(http_server)
EventRestServlet(hs).register(http_server)

View File

@@ -15,12 +15,13 @@
from twisted.internet import defer
from synapse.api.streams import PaginationConfig
from synapse.streams.config import PaginationConfig
from base import RestServlet, client_path_pattern
class ImSyncRestServlet(RestServlet):
PATTERN = client_path_pattern("/im/sync$")
# TODO: Needs unit testing
class InitialSyncRestServlet(RestServlet):
PATTERN = client_path_pattern("/initialSync$")
@defer.inlineCallbacks
def on_GET(self, request):
@@ -37,4 +38,4 @@ class ImSyncRestServlet(RestServlet):
def register_servlets(hs, http_server):
ImSyncRestServlet(hs).register(http_server)
InitialSyncRestServlet(hs).register(http_server)

View File

@@ -27,7 +27,7 @@ class LoginRestServlet(RestServlet):
PASS_TYPE = "m.login.password"
def on_GET(self, request):
return (200, {"type": LoginRestServlet.PASS_TYPE})
return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]})
def on_OPTIONS(self, request):
return (200, {})

View File

@@ -68,7 +68,7 @@ class PresenceStatusRestServlet(RestServlet):
class PresenceListRestServlet(RestServlet):
PATTERN = client_path_pattern("/presence_list/(?P<user_id>[^/]*)")
PATTERN = client_path_pattern("/presence/list/(?P<user_id>[^/]*)")
@defer.inlineCallbacks
def on_GET(self, request, user_id):

View File

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

View File

@@ -18,10 +18,9 @@ from twisted.internet import defer
from base import RestServlet, client_path_pattern
from synapse.api.errors import SynapseError, Codes
from synapse.api.events.room import (RoomTopicEvent, MessageEvent,
RoomMemberEvent, FeedbackEvent)
from synapse.api.constants import Feedback, Membership
from synapse.api.streams import PaginationConfig
from synapse.streams.config import PaginationConfig
from synapse.api.events.room import RoomMemberEvent
from synapse.api.constants import Membership
import json
import logging
@@ -35,31 +34,28 @@ class RoomCreateRestServlet(RestServlet):
# No PATTERN; we have custom dispatch rules here
def register(self, http_server):
# /rooms OR /rooms/<roomid>
http_server.register_path("POST",
client_path_pattern("/rooms$"),
self.on_POST)
http_server.register_path("PUT",
client_path_pattern(
"/rooms/(?P<room_id>[^/]*)$"),
self.on_PUT)
PATTERN = "/createRoom"
register_txn_path(self, PATTERN, http_server)
# define CORS for all of /rooms in RoomCreateRestServlet for simplicity
http_server.register_path("OPTIONS",
client_path_pattern("/rooms(?:/.*)?$"),
self.on_OPTIONS)
# define CORS for /createRoom[/txnid]
http_server.register_path("OPTIONS",
client_path_pattern("/createRoom(?:/.*)?$"),
self.on_OPTIONS)
@defer.inlineCallbacks
def on_PUT(self, request, room_id):
room_id = urllib.unquote(room_id)
auth_user = yield self.auth.get_user_by_req(request)
def on_PUT(self, request, txn_id):
try:
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
except KeyError:
pass
if not room_id:
raise SynapseError(400, "PUT must specify a room ID")
response = yield self.on_POST(request)
room_config = self.get_room_config(request)
info = yield self.make_room(room_config, auth_user, room_id)
room_config.update(info)
defer.returnValue((200, info))
self.txns.store_client_transaction(request, txn_id, response)
defer.returnValue(response)
@defer.inlineCallbacks
def on_POST(self, request):
@@ -95,254 +91,193 @@ class RoomCreateRestServlet(RestServlet):
return (200, {})
class RoomTopicRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/topic$")
# TODO: Needs unit testing for generic events
class RoomStateEventRestServlet(RestServlet):
def register(self, http_server):
# /room/$roomid/state/$eventtype
no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$"
def get_event_type(self):
return RoomTopicEvent.TYPE
# /room/$roomid/state/$eventtype/$statekey
state_key = ("/rooms/(?P<room_id>[^/]*)/state/" +
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$")
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):
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=urllib.unquote(room_id),
event_type=RoomTopicEvent.TYPE,
state_key="",
event_type=urllib.unquote(event_type),
state_key=urllib.unquote(state_key),
)
if not data:
raise SynapseError(404, "Topic not found.", errcode=Codes.NOT_FOUND)
defer.returnValue((200, data.content))
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
defer.returnValue((200, data[0].get_dict()["content"]))
@defer.inlineCallbacks
def on_PUT(self, request, room_id):
def on_PUT(self, request, room_id, event_type, state_key):
user = yield self.auth.get_user_by_req(request)
event_type = urllib.unquote(event_type)
content = _parse_json(request)
event = self.event_factory.create_event(
etype=self.get_event_type(),
etype=event_type,
content=content,
room_id=urllib.unquote(room_id),
user_id=user.to_string(),
state_key=urllib.unquote(state_key)
)
msg_handler = self.handlers.message_handler
yield msg_handler.store_room_data(
event=event
)
defer.returnValue((200, ""))
class JoinRoomAliasServlet(RestServlet):
PATTERN = client_path_pattern("/join/(?P<room_alias>[^/]+)$")
@defer.inlineCallbacks
def on_PUT(self, request, room_alias):
user = yield self.auth.get_user_by_req(request)
if not user:
defer.returnValue((403, "Unrecognized user"))
logger.debug("room_alias: %s", room_alias)
room_alias = self.hs.parse_roomalias(urllib.unquote(room_alias))
handler = self.handlers.room_member_handler
ret_dict = yield handler.join_room_alias(user, room_alias)
defer.returnValue((200, ret_dict))
class RoomMemberRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members/"
+ "(?P<target_user_id>[^/]*)/state$")
def get_event_type(self):
return RoomMemberEvent.TYPE
@defer.inlineCallbacks
def on_GET(self, request, room_id, target_user_id):
room_id = urllib.unquote(room_id)
user = yield self.auth.get_user_by_req(request)
handler = self.handlers.room_member_handler
member = yield handler.get_room_member(
room_id,
urllib.unquote(target_user_id),
user.to_string())
if not member:
raise SynapseError(404, "Member not found.",
errcode=Codes.NOT_FOUND)
defer.returnValue((200, member.content))
@defer.inlineCallbacks
def on_DELETE(self, request, roomid, target_user_id):
user = yield self.auth.get_user_by_req(request)
event = self.event_factory.create_event(
etype=self.get_event_type(),
target_user_id=urllib.unquote(target_user_id),
room_id=urllib.unquote(roomid),
user_id=user.to_string(),
membership=Membership.LEAVE,
content={"membership": Membership.LEAVE}
if event_type == RoomMemberEvent.TYPE:
# membership events are special
handler = self.handlers.room_member_handler
yield handler.change_membership(event)
defer.returnValue((200, ""))
else:
# store random bits of state
msg_handler = self.handlers.message_handler
yield msg_handler.store_room_data(
event=event
)
defer.returnValue((200, ""))
handler = self.handlers.room_member_handler
yield handler.change_membership(event)
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<room_id>[^/]*)/send/(?P<event_type>[^/]*)")
register_txn_path(self, PATTERN, http_server, with_get=True)
@defer.inlineCallbacks
def on_PUT(self, request, roomid, target_user_id):
def on_POST(self, request, room_id, event_type):
user = yield self.auth.get_user_by_req(request)
content = _parse_json(request)
if "membership" not in content:
raise SynapseError(400, "No membership key.",
errcode=Codes.BAD_JSON)
valid_membership_values = [Membership.JOIN, Membership.INVITE]
if (content["membership"] not in valid_membership_values):
raise SynapseError(400, "Membership value must be %s." % (
valid_membership_values,), errcode=Codes.BAD_JSON)
event = self.event_factory.create_event(
etype=self.get_event_type(),
target_user_id=urllib.unquote(target_user_id),
room_id=urllib.unquote(roomid),
user_id=user.to_string(),
membership=content["membership"],
content=content
)
handler = self.handlers.room_member_handler
yield handler.change_membership(event)
defer.returnValue((200, ""))
class MessageRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages/"
+ "(?P<sender_id>[^/]*)/(?P<msg_id>[^/]*)$")
def get_event_type(self):
return MessageEvent.TYPE
@defer.inlineCallbacks
def on_GET(self, request, room_id, sender_id, msg_id):
user = yield self.auth.get_user_by_req(request)
msg_handler = self.handlers.message_handler
msg = yield msg_handler.get_message(room_id=urllib.unquote(room_id),
sender_id=urllib.unquote(sender_id),
msg_id=msg_id,
user_id=user.to_string(),
)
if not msg:
raise SynapseError(404, "Message not found.",
errcode=Codes.NOT_FOUND)
defer.returnValue((200, json.loads(msg.content)))
@defer.inlineCallbacks
def on_PUT(self, request, room_id, sender_id, msg_id):
user = yield self.auth.get_user_by_req(request)
if user.to_string() != urllib.unquote(sender_id):
raise SynapseError(403, "Must send messages as yourself.",
errcode=Codes.FORBIDDEN)
content = _parse_json(request)
event = self.event_factory.create_event(
etype=self.get_event_type(),
etype=event_type,
room_id=urllib.unquote(room_id),
user_id=user.to_string(),
msg_id=msg_id,
content=content
)
)
msg_handler = self.handlers.message_handler
yield msg_handler.send_message(event)
defer.returnValue((200, ""))
defer.returnValue((200, {"event_id": event.event_id}))
class FeedbackRestServlet(RestServlet):
PATTERN = client_path_pattern(
"/rooms/(?P<room_id>[^/]*)/messages/" +
"(?P<msg_sender_id>[^/]*)/(?P<msg_id>[^/]*)/feedback/" +
"(?P<sender_id>[^/]*)/(?P<feedback_type>[^/]*)$"
)
def get_event_type(self):
return FeedbackEvent.TYPE
def on_GET(self, request, room_id, event_type, txn_id):
return (200, "Not implemented")
@defer.inlineCallbacks
def on_GET(self, request, room_id, msg_sender_id, msg_id, fb_sender_id,
feedback_type):
user = yield (self.auth.get_user_by_req(request))
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
# TODO (erikj): Implement this?
raise NotImplementedError("Getting feedback is not supported")
response = yield self.on_POST(request, room_id, event_type)
# if feedback_type not in Feedback.LIST:
# raise SynapseError(400, "Bad feedback type.",
# errcode=Codes.BAD_JSON)
#
# msg_handler = self.handlers.message_handler
# feedback = yield msg_handler.get_feedback(
# room_id=urllib.unquote(room_id),
# msg_sender_id=msg_sender_id,
# msg_id=msg_id,
# user_id=user.to_string(),
# fb_sender_id=fb_sender_id,
# fb_type=feedback_type
# )
#
# if not feedback:
# raise SynapseError(404, "Feedback not found.",
# errcode=Codes.NOT_FOUND)
#
# defer.returnValue((200, json.loads(feedback.content)))
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<room_identifier>[^/]*)")
register_txn_path(self, PATTERN, http_server)
@defer.inlineCallbacks
def on_PUT(self, request, room_id, sender_id, msg_id, fb_sender_id,
feedback_type):
user = yield (self.auth.get_user_by_req(request))
def on_POST(self, request, room_identifier):
user = yield self.auth.get_user_by_req(request)
if user.to_string() != fb_sender_id:
raise SynapseError(403, "Must send feedback as yourself.",
errcode=Codes.FORBIDDEN)
# 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.
if feedback_type not in Feedback.LIST:
raise SynapseError(400, "Bad feedback type.",
errcode=Codes.BAD_JSON)
content = _parse_json(request)
event = self.event_factory.create_event(
etype=self.get_event_type(),
room_id=urllib.unquote(room_id),
msg_sender_id=sender_id,
msg_id=msg_id,
user_id=user.to_string(), # user sending the feedback
feedback_type=feedback_type,
content=content
identifier = None
is_room_alias = False
try:
identifier = self.hs.parse_roomalias(
urllib.unquote(room_identifier)
)
is_room_alias = True
except SynapseError:
identifier = self.hs.parse_roomid(
urllib.unquote(room_identifier)
)
msg_handler = self.handlers.message_handler
yield msg_handler.send_feedback(event)
# TODO: Support for specifying the home server to join with?
defer.returnValue((200, ""))
if is_room_alias:
handler = self.handlers.room_member_handler
ret_dict = yield handler.join_room_alias(user, identifier)
defer.returnValue((200, ret_dict))
else: # room id
event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
content={"membership": Membership.JOIN},
room_id=urllib.unquote(identifier.to_string()),
user_id=user.to_string(),
state_key=user.to_string()
)
handler = self.handlers.room_member_handler
yield handler.change_membership(event)
defer.returnValue((200, ""))
@defer.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<room_id>[^/]*)/members/list$")
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members$")
@defer.inlineCallbacks
def on_GET(self, request, room_id):
@@ -354,7 +289,8 @@ class RoomMemberListRestServlet(RestServlet):
user_id=user.to_string())
for event in members["chunk"]:
target_user = self.hs.parse_userid(event["target_user_id"])
# 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_state = yield self.handlers.presence_handler.get_state(
@@ -367,8 +303,9 @@ class RoomMemberListRestServlet(RestServlet):
defer.returnValue((200, members))
# TODO: Needs unit testing
class RoomMessageListRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages/list$")
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages$")
@defer.inlineCallbacks
def on_GET(self, request, room_id):
@@ -385,6 +322,50 @@ class RoomMessageListRestServlet(RestServlet):
defer.returnValue((200, msgs))
# TODO: Needs unit testing
class RoomStateRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/state$")
@defer.inlineCallbacks
def on_GET(self, request, room_id):
user = yield self.auth.get_user_by_req(request)
# TODO: Get all the current state for this room and return in the same
# format as initial sync, that is:
# [
# { state event }, { state event }
# ]
defer.returnValue((200, []))
# TODO: Needs unit testing
class RoomInitialSyncRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/initialSync$")
@defer.inlineCallbacks
def on_GET(self, request, room_id):
user = yield self.auth.get_user_by_req(request)
# TODO: Get all the initial sync data for this room and return in the
# same format as initial sync, that is:
# {
# membership: join,
# messages: [
# chunk: [ msg events ],
# start: s_tok,
# end: e_tok
# ],
# room_id: foo,
# state: [
# { state event } , { state event }
# ]
# }
# Probably worth keeping the keys room_id and membership for parity with
# /initialSync even though they must be joined to sync this and know the
# room ID, so clients can reuse the same code (room_id and membership
# are MANDATORY for /initialSync, so the code will expect it to be
# there)
defer.returnValue((200, {}))
class RoomTriggerBackfill(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/backfill$")
@@ -400,6 +381,53 @@ class RoomTriggerBackfill(RestServlet):
res = [event.get_dict() 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<room_id>[^/]*)/" +
"(?P<membership_action>join|invite|leave)")
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 == "invite":
if "user_id" not in content:
raise SynapseError(400, "Missing user_id key.")
state_key = content["user_id"]
event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
content={"membership": unicode(membership_action)},
room_id=urllib.unquote(room_id),
user_id=user.to_string(),
state_key=state_key
)
handler = self.handlers.room_member_handler
yield handler.change_membership(event)
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)
def _parse_json(request):
try:
content = json.loads(request.content.read())
@@ -411,13 +439,46 @@ def _parse_json(request):
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<txn_id>[^/]*)$"),
servlet.on_PUT
)
if with_get:
http_server.register_path(
"GET",
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
servlet.on_GET
)
def register_servlets(hs, http_server):
RoomTopicRestServlet(hs).register(http_server)
RoomMemberRestServlet(hs).register(http_server)
MessageRestServlet(hs).register(http_server)
FeedbackRestServlet(hs).register(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)

View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module contains 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__)
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. Typically
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. Typically
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

View File

@@ -20,18 +20,18 @@
# Imports required for the default HomeServer() implementation
from synapse.federation import initialize_http_replication
from synapse.federation.handler import FederationEventHandler
from synapse.api.events.factory import EventFactory
from synapse.api.notifier import Notifier
from synapse.notifier import Notifier
from synapse.api.auth import Auth
from synapse.handlers import Handlers
from synapse.rest import RestServletFactory
from synapse.state import StateHandler
from synapse.storage import DataStore
from synapse.types import UserID, RoomAlias
from synapse.types import UserID, RoomAlias, RoomID
from synapse.util import Clock
from synapse.util.distributor import Distributor
from synapse.util.lockutils import LockManager
from synapse.streams.events import EventSources
class BaseHomeServer(object):
@@ -58,7 +58,6 @@ class BaseHomeServer(object):
'http_client',
'db_pool',
'persistence_service',
'federation',
'replication_layer',
'datastore',
'event_factory',
@@ -73,6 +72,7 @@ class BaseHomeServer(object):
'resource_for_federation',
'resource_for_web_client',
'resource_for_content_repo',
'event_sources',
]
def __init__(self, hostname, **kwargs):
@@ -117,6 +117,9 @@ class BaseHomeServer(object):
setattr(BaseHomeServer, "get_%s" % (depname), _get)
# TODO: Why are these parse_ methods so high up along with other globals?
# Surely these should be in a util package or in the api package?
# Other utility methods
def parse_userid(self, s):
"""Parse the string given by 's' as a User ID and return a UserID
@@ -128,6 +131,11 @@ class BaseHomeServer(object):
object."""
return RoomAlias.from_string(s, hs=self)
def parse_roomid(self, s):
"""Parse the string given by 's' as a Room ID and return a RoomID
object."""
return RoomID.from_string(s, hs=self)
# Build magic accessors for every dependency
for depname in BaseHomeServer.DEPENDENCIES:
BaseHomeServer._make_dependency_method(depname)
@@ -152,9 +160,6 @@ class HomeServer(BaseHomeServer):
def build_replication_layer(self):
return initialize_http_replication(self)
def build_federation(self):
return FederationEventHandler(self)
def build_datastore(self):
return DataStore(self)
@@ -182,6 +187,9 @@ class HomeServer(BaseHomeServer):
def build_distributor(self):
return Distributor()
def build_event_sources(self):
return EventSources(self)
def register_servlets(self):
""" Register all servlets associated with this HomeServer.
"""

View File

@@ -45,7 +45,7 @@ class StateHandler(object):
@defer.inlineCallbacks
@log_function
def handle_new_event(self, event):
def handle_new_event(self, event, snapshot):
""" Given an event this works out if a) we have sufficient power level
to update the state and b) works out what the prev_state should be.
@@ -70,25 +70,13 @@ class StateHandler(object):
# Now I need to fill out the prev state and work out if it has auth
# (w.r.t. to power levels)
results = yield self.store.get_latest_pdus_in_context(
event.room_id
)
snapshot.fill_out_prev_events(event)
event.prev_events = [
encode_event_id(p_id, origin) for p_id, origin, _ in results
]
event.prev_events = [
e for e in event.prev_events if e != event.event_id
]
if results:
event.depth = max([int(v) for _, _, v in results]) + 1
else:
event.depth = 0
current_state = yield self.store.get_current_state_pdu(
key.context, key.type, key.state_key
)
current_state = snapshot.prev_state_pdu
if current_state:
event.prev_state = encode_event_id(

View File

@@ -16,8 +16,9 @@
from twisted.internet import defer
from synapse.api.events.room import (
RoomMemberEvent, MessageEvent, RoomTopicEvent, FeedbackEvent,
RoomConfigEvent, RoomNameEvent,
RoomMemberEvent, RoomTopicEvent, FeedbackEvent,
# RoomConfigEvent,
RoomNameEvent,
)
from synapse.util.logutils import log_function
@@ -56,20 +57,22 @@ class DataStore(RoomMemberStore, RoomStore,
@defer.inlineCallbacks
@log_function
def persist_event(self, event, backfilled=False):
if event.type == RoomMemberEvent.TYPE:
yield self._store_room_member(event)
elif event.type == FeedbackEvent.TYPE:
yield self._store_feedback(event)
# elif event.type == RoomConfigEvent.TYPE:
# yield self._store_room_config(event)
elif event.type == RoomNameEvent.TYPE:
yield self._store_room_name(event)
elif event.type == RoomTopicEvent.TYPE:
yield self._store_room_topic(event)
def persist_event(self, event=None, backfilled=False, pdu=None):
stream_ordering = None
if backfilled:
if not self.min_token_deferred.called:
yield self.min_token_deferred
self.min_token -= 1
stream_ordering = self.min_token
ret = yield self._store_event(event, backfilled)
defer.returnValue(ret)
latest = yield self._db_pool.runInteraction(
self._persist_pdu_event_txn,
pdu=pdu,
event=event,
backfilled=backfilled,
stream_ordering=stream_ordering,
)
defer.returnValue(latest)
@defer.inlineCallbacks
def get_event(self, event_id):
@@ -79,7 +82,6 @@ class DataStore(RoomMemberStore, RoomStore,
[
"event_id",
"type",
"sender",
"room_id",
"content",
"unrecognized_keys"
@@ -89,12 +91,44 @@ class DataStore(RoomMemberStore, RoomStore,
event = self._parse_event_from_row(events_dict)
defer.returnValue(event)
@defer.inlineCallbacks
def _persist_pdu_event_txn(self, txn, pdu=None, event=None,
backfilled=False, stream_ordering=None):
if pdu is not None:
self._persist_event_pdu_txn(txn, pdu)
if event is not None:
return self._persist_event_txn(
txn, event, backfilled, stream_ordering
)
def _persist_event_pdu_txn(self, txn, pdu):
cols = dict(pdu.__dict__)
unrec_keys = dict(pdu.unrecognized_keys)
del cols["content"]
del cols["prev_pdus"]
cols["content_json"] = json.dumps(pdu.content)
cols["unrecognized_keys"] = json.dumps(unrec_keys)
logger.debug("Persisting: %s", repr(cols))
if pdu.is_state:
self._persist_state_txn(txn, pdu.prev_pdus, cols)
else:
self._persist_pdu_txn(txn, pdu.prev_pdus, cols)
self._update_min_depth_for_context_txn(txn, pdu.context, pdu.depth)
@log_function
def _store_event(self, event, backfilled):
# FIXME (erikj): This should be removed when we start amalgamating
# event and pdu storage
yield self.hs.get_federation().fill_out_prev_events(event)
def _persist_event_txn(self, txn, event, backfilled, stream_ordering=None):
if event.type == RoomMemberEvent.TYPE:
self._store_room_member_txn(txn, event)
elif event.type == FeedbackEvent.TYPE:
self._store_feedback_txn(txn,event)
# elif event.type == RoomConfigEvent.TYPE:
# self._store_room_config_txn(txn, event)
elif event.type == RoomNameEvent.TYPE:
self._store_room_name_txn(txn, event)
elif event.type == RoomTopicEvent.TYPE:
self._store_room_topic_txn(txn, event)
vals = {
"topological_ordering": event.depth,
@@ -105,17 +139,14 @@ class DataStore(RoomMemberStore, RoomStore,
"processed": True,
}
if stream_ordering is not None:
vals["stream_ordering"] = stream_ordering
if hasattr(event, "outlier"):
vals["outlier"] = event.outlier
else:
vals["outlier"] = False
if backfilled:
if not self.min_token_deferred.called:
yield self.min_token_deferred
self.min_token -= 1
vals["stream_ordering"] = self.min_token
unrec = {
k: v
for k, v in event.get_full_dict().items()
@@ -124,7 +155,7 @@ class DataStore(RoomMemberStore, RoomStore,
vals["unrecognized_keys"] = json.dumps(unrec)
try:
yield self._simple_insert("events", vals)
self._simple_insert_txn(txn, "events", vals)
except:
logger.exception(
"Failed to persist, probably duplicate: %s",
@@ -143,9 +174,10 @@ class DataStore(RoomMemberStore, RoomStore,
if hasattr(event, "prev_state"):
vals["prev_state"] = event.prev_state
yield self._simple_insert("state_events", vals)
self._simple_insert_txn(txn, "state_events", vals)
yield self._simple_insert(
self._simple_insert_txn(
txn,
"current_state_events",
{
"event_id": event.event_id,
@@ -155,8 +187,7 @@ class DataStore(RoomMemberStore, RoomStore,
}
)
latest = yield self.get_room_events_max_id()
defer.returnValue(latest)
return self._get_room_events_max_id_txn(txn)
@defer.inlineCallbacks
def get_current_state(self, room_id, event_type=None, state_key=""):
@@ -192,6 +223,85 @@ class DataStore(RoomMemberStore, RoomStore,
defer.returnValue(self.min_token)
def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
"""Snapshot the room for an update by a user
Args:
room_id (synapse.types.RoomId): The room to snapshot.
user_id (synapse.types.UserId): The user to snapshot the room for.
state_type (str): Optional state type to snapshot.
state_key (str): Optional state key to snapshot.
Returns:
synapse.storage.Snapshot: A snapshot of the state of the room.
"""
def _snapshot(txn):
membership_state = self._get_room_member(txn, user_id, room_id)
prev_pdus = self._get_latest_pdus_in_context(
txn, room_id
)
if state_type is not None and state_key is not None:
prev_state_pdu = self._get_current_state_pdu(
txn, room_id, state_type, state_key
)
else:
prev_state_pdu = None
return Snapshot(
store=self,
room_id=room_id,
user_id=user_id,
prev_pdus=prev_pdus,
membership_state=membership_state,
state_type=state_type,
state_key=state_key,
prev_state_pdu=prev_state_pdu,
)
return self._db_pool.runInteraction(_snapshot)
class Snapshot(object):
"""Snapshot of the state of a room
Args:
store (DataStore): The datastore.
room_id (RoomId): The room of the snapshot.
user_id (UserId): The user this snapshot is for.
prev_pdus (list): The list of PDU ids this snapshot is after.
membership_state (RoomMemberEvent): The current state of the user in
the room.
state_type (str, optional): State type captured by the snapshot
state_key (str, optional): State key captured by the snapshot
prev_state_pdu (PduEntry, optional): pdu id of
the previous value of the state type and key in the room.
"""
def __init__(self, store, room_id, user_id, prev_pdus,
membership_state, state_type=None, state_key=None,
prev_state_pdu=None):
self.store = store
self.room_id = room_id
self.user_id = user_id
self.prev_pdus = prev_pdus
self.membership_state = membership_state
self.state_type = state_type
self.state_key = state_key
self.prev_state_pdu = prev_state_pdu
def fill_out_prev_events(self, event):
if hasattr(event, "prev_events"):
return
es = [
"%s@%s" % (p_id, origin) for p_id, origin, _ in self.prev_pdus
]
event.prev_events = [e for e in es if e != event.event_id]
if self.prev_pdus:
event.depth = max([int(v) for _, _, v in self.prev_pdus]) + 1
else:
event.depth = 0
def schema_path(schema):
""" Get a filesystem path for the named database schema

View File

@@ -86,16 +86,18 @@ class SQLBaseStore(object):
table : string giving the table name
values : dict of new column names and values for them
"""
return self._db_pool.runInteraction(
self._simple_insert_txn, table, values,
)
def _simple_insert_txn(self, txn, table, values):
sql = "INSERT INTO %s (%s) VALUES(%s)" % (
table,
", ".join(k for k in values),
", ".join("?" for k in values)
)
def func(txn):
txn.execute(sql, values.values())
return txn.lastrowid
return self._db_pool.runInteraction(func)
txn.execute(sql, values.values())
return txn.lastrowid
def _simple_select_one(self, table, keyvalues, retcols,
allow_none=False):

View File

@@ -15,21 +15,17 @@
from twisted.internet import defer
from ._base import SQLBaseStore, Table
from synapse.api.events.room import FeedbackEvent
import collections
import json
from ._base import SQLBaseStore
class FeedbackStore(SQLBaseStore):
def _store_feedback(self, event):
return self._simple_insert("feedback", {
def _store_feedback_txn(self, txn, event):
self._simple_insert_txn(txn, "feedback", {
"event_id": event.event_id,
"feedback_type": event.feedback_type,
"feedback_type": event.content["type"],
"room_id": event.room_id,
"target_event_id": event.target_event,
"target_event_id": event.content["target_event_id"],
"sender": event.user_id,
})

View File

@@ -114,7 +114,7 @@ class PduStore(SQLBaseStore):
return self._get_pdu_tuples(txn, res)
def persist_pdu(self, prev_pdus, **cols):
def _persist_pdu_txn(self, txn, prev_pdus, cols):
"""Inserts a (non-state) PDU into the database.
Args:
@@ -122,11 +122,6 @@ class PduStore(SQLBaseStore):
prev_pdus (list)
**cols: The columns to insert into the PdusTable.
"""
return self._db_pool.runInteraction(
self._persist_pdu, prev_pdus, cols
)
def _persist_pdu(self, txn, prev_pdus, cols):
entry = PdusTable.EntryType(
**{k: cols.get(k, None) for k in PdusTable.fields}
)
@@ -262,7 +257,7 @@ class PduStore(SQLBaseStore):
return row[0] if row else None
def update_min_depth_for_context(self, context, depth):
def _update_min_depth_for_context_txn(self, txn, context, depth):
"""Update the minimum `depth` of the given context, which is the line
on which we stop backfilling backwards.
@@ -270,11 +265,6 @@ class PduStore(SQLBaseStore):
context (str)
depth (int)
"""
return self._db_pool.runInteraction(
self._update_min_depth_for_context, context, depth
)
def _update_min_depth_for_context(self, txn, context, depth):
min_depth = self._get_min_depth_interaction(txn, context)
do_insert = depth < min_depth if min_depth else True
@@ -286,7 +276,7 @@ class PduStore(SQLBaseStore):
(context, depth)
)
def get_latest_pdus_in_context(self, context):
def _get_latest_pdus_in_context(self, txn, context):
"""Get's a list of the most current pdus for a given context. This is
used when we are sending a Pdu and need to fill out the `prev_pdus`
key
@@ -295,11 +285,6 @@ class PduStore(SQLBaseStore):
txn
context
"""
return self._db_pool.runInteraction(
self._get_latest_pdus_in_context, context
)
def _get_latest_pdus_in_context(self, txn, context):
query = (
"SELECT p.pdu_id, p.origin, p.depth FROM %(pdus)s as p "
"INNER JOIN %(forward)s as f ON p.pdu_id = f.pdu_id "
@@ -485,7 +470,7 @@ class StatePduStore(SQLBaseStore):
"""A collection of queries for handling state PDUs.
"""
def persist_state(self, prev_pdus, **cols):
def _persist_state_txn(self, txn, prev_pdus, cols):
"""Inserts a state PDU into the database
Args:
@@ -493,12 +478,6 @@ class StatePduStore(SQLBaseStore):
prev_pdus (list)
**cols: The columns to insert into the PdusTable and StatePdusTable
"""
return self._db_pool.runInteraction(
self._persist_state, prev_pdus, cols
)
def _persist_state(self, txn, prev_pdus, cols):
pdu_entry = PdusTable.EntryType(
**{k: cols.get(k, None) for k in PdusTable.fields}
)

View File

@@ -18,12 +18,10 @@ from twisted.internet import defer
from sqlite3 import IntegrityError
from synapse.api.errors import StoreError
from synapse.api.events.room import RoomTopicEvent
from ._base import SQLBaseStore, Table
import collections
import json
import logging
logger = logging.getLogger(__name__)
@@ -131,8 +129,9 @@ class RoomStore(SQLBaseStore):
defer.returnValue(ret)
def _store_room_topic(self, event):
return self._simple_insert(
def _store_room_topic_txn(self, txn, event):
self._simple_insert_txn(
txn,
"topics",
{
"event_id": event.event_id,
@@ -141,8 +140,9 @@ class RoomStore(SQLBaseStore):
}
)
def _store_room_name(self, event):
return self._simple_insert(
def _store_room_name_txn(self, txn, event):
self._simple_insert_txn(
txn,
"room_names",
{
"event_id": event.event_id,

View File

@@ -15,15 +15,10 @@
from twisted.internet import defer
from synapse.types import UserID
from ._base import SQLBaseStore
from synapse.api.constants import Membership
from synapse.api.events.room import RoomMemberEvent
from ._base import SQLBaseStore, Table
import collections
import json
import logging
logger = logging.getLogger(__name__)
@@ -31,17 +26,18 @@ logger = logging.getLogger(__name__)
class RoomMemberStore(SQLBaseStore):
@defer.inlineCallbacks
def _store_room_member(self, event):
def _store_room_member_txn(self, txn, event):
"""Store a room member in the database.
"""
domain = self.hs.parse_userid(event.target_user_id).domain
target_user_id = event.state_key
domain = self.hs.parse_userid(target_user_id).domain
yield self._simple_insert(
self._simple_insert_txn(
txn,
"room_memberships",
{
"event_id": event.event_id,
"user_id": event.target_user_id,
"user_id": target_user_id,
"sender": event.user_id,
"room_id": event.room_id,
"membership": event.membership,
@@ -54,13 +50,13 @@ class RoomMemberStore(SQLBaseStore):
"INSERT OR IGNORE INTO room_hosts (room_id, host) "
"VALUES (?, ?)"
)
yield self._execute(None, sql, event.room_id, domain)
txn.execute(sql, (event.room_id, domain))
else:
sql = (
"DELETE FROM room_hosts WHERE room_id = ? AND host = ?"
)
yield self._execute(None, sql, event.room_id, domain)
txn.execute(sql, (event.room_id, domain))
@defer.inlineCallbacks
def get_room_member(self, user_id, room_id):
@@ -79,6 +75,24 @@ class RoomMemberStore(SQLBaseStore):
defer.returnValue(rows[0] if rows else None)
def _get_room_member(self, txn, user_id, room_id):
sql = (
"SELECT e.* FROM events as e"
" INNER JOIN room_memberships as m"
" ON e.event_id = m.event_id"
" INNER JOIN current_state_events as c"
" ON m.event_id = c.event_id"
" WHERE m.user_id = ? and e.room_id = ?"
" LIMIT 1"
)
txn.execute(sql, (user_id, room_id))
rows = self.cursor_to_dict(txn)
if rows:
return self._parse_event_from_row(rows[0])
else:
return None
def get_room_members(self, room_id, membership=None):
"""Retrieve the current room member list for a room.
@@ -149,3 +163,24 @@ class RoomMemberStore(SQLBaseStore):
results = [self._parse_event_from_row(r) for r in rows]
defer.returnValue(results)
@defer.inlineCallbacks
def do_users_share_a_room(self, user_list):
""" Checks whether a list of users share a room.
"""
user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_list))
sql = (
"SELECT m.room_id FROM room_memberships as m "
"INNER JOIN current_state_events as c "
"ON m.event_id = c.event_id "
"WHERE m.membership = 'join' "
"AND (%(clause)s) "
"GROUP BY m.room_id HAVING COUNT(m.room_id) = ?"
) % {"clause": user_list_clause}
args = user_list
args.append(len(user_list))
rows = yield self._execute(None, sql, *args)
defer.returnValue(len(rows) > 0)

View File

@@ -98,5 +98,6 @@ CREATE TABLE IF NOT EXISTS rooms(
CREATE TABLE IF NOT EXISTS room_hosts(
room_id TEXT NOT NULL,
host TEXT NOT NULL
host TEXT NOT NULL,
CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
);

View File

@@ -37,10 +37,8 @@ from twisted.internet import defer
from ._base import SQLBaseStore
from synapse.api.errors import SynapseError
from synapse.api.constants import Membership
from synapse.util.logutils import log_function
import json
import logging
@@ -176,7 +174,7 @@ class StreamStore(SQLBaseStore):
"SELECT * FROM events as e WHERE "
"((room_id IN (%(current)s)) OR "
"(event_id IN (%(invites)s))) "
"AND e.stream_ordering > ? AND e.stream_ordering < ? "
"AND e.stream_ordering > ? AND e.stream_ordering <= ? "
"AND e.outlier = 0 "
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
) % {
@@ -207,8 +205,11 @@ class StreamStore(SQLBaseStore):
with_feedback=False):
# TODO (erikj): Handle compressed feedback
from_comp = '<' if direction =='b' else '>'
to_comp = '>' if direction =='b' else '<'
# Tokens really represent positions between elements, but we use
# the convention of pointing to the event before the gap. Hence
# we have a bit of asymmetry when it comes to equalities.
from_comp = '<=' if direction =='b' else '>'
to_comp = '>' if direction =='b' else '<='
order = "DESC" if direction == 'b' else "ASC"
args = [room_id]
@@ -257,7 +258,7 @@ class StreamStore(SQLBaseStore):
sql = (
"SELECT * FROM events "
"WHERE room_id = ? AND stream_ordering <= ? "
"ORDER BY topological_ordering, stream_ordering DESC LIMIT ? "
"ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? "
)
rows = yield self._execute_and_decode(
@@ -283,17 +284,20 @@ class StreamStore(SQLBaseStore):
)
)
@defer.inlineCallbacks
def get_room_events_max_id(self):
res = yield self._execute_and_decode(
return self._db_pool.runInteraction(self._get_room_events_max_id_txn)
def _get_room_events_max_id_txn(self, txn):
txn.execute(
"SELECT MAX(stream_ordering) as m FROM events"
)
res = self.cursor_to_dict(txn)
logger.debug("get_room_events_max_id: %s", res)
if not res or not res[0] or not res[0]["m"]:
defer.returnValue("s1")
return
return "s0"
key = res[0]["m"] + 1
defer.returnValue("s%d" % (key,))
key = res[0]["m"]
return "s%d" % (key,)

View File

@@ -12,22 +12,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module contains REST servlets to do with public paths: /public"""
from twisted.internet import defer
from base import RestServlet, client_path_pattern
class PublicRoomListRestServlet(RestServlet):
PATTERN = client_path_pattern("/public/rooms$")
@defer.inlineCallbacks
def on_GET(self, request):
handler = self.handlers.room_list_handler
data = yield handler.get_public_room_list()
defer.returnValue((200, data))
def register_servlets(hs, http_server):
PublicRoomListRestServlet(hs).register(http_server)

84
synapse/streams/config.py Normal file
View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.api.errors import SynapseError
from synapse.types import StreamToken
import logging
logger = logging.getLogger(__name__)
class PaginationConfig(object):
"""A configuration object which stores pagination parameters."""
def __init__(self, from_token=None, to_token=None, direction='f',
limit=0):
self.from_token = from_token
self.to_token = to_token
self.direction = 'f' if direction == 'f' else 'b'
self.limit = int(limit)
@classmethod
def from_request(cls, request, raise_invalid_params=True):
def get_param(name, default=None):
lst = request.args.get(name, [])
if len(lst) > 1:
raise SynapseError(
400, "%s must be specified only once" % (name,)
)
elif len(lst) == 1:
return lst[0]
else:
return default
direction = get_param("dir", 'f')
if direction not in ['f', 'b']:
raise SynapseError(400, "'dir' parameter is invalid.")
from_tok = get_param("from")
to_tok = get_param("to")
try:
if from_tok == "END":
from_tok = None # For backwards compat.
elif from_tok:
from_tok = StreamToken.from_string(from_tok)
except:
raise SynapseError(400, "'from' paramater is invalid")
try:
if to_tok:
to_tok = StreamToken.from_string(to_tok)
except:
raise SynapseError(400, "'to' paramater is invalid")
limit = get_param("limit", "0")
if not limit.isdigit():
raise SynapseError(400, "'limit' parameter must be an integer.")
try:
return PaginationConfig(from_tok, to_tok, direction, limit)
except:
logger.exception("Failed to create pagination config")
raise SynapseError(400, "Invalid request.")
def __str__(self):
return (
"<PaginationConfig from_tok=%s, to_tok=%s, "
"direction=%s, limit=%s>"
) % (self.from_token, self.to_token, self.direction, self.limit)

195
synapse/streams/events.py Normal file
View File

@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.internet import defer
from synapse.types import StreamToken
class NullSource(object):
"""This event source never yields any events and its token remains at
zero. It may be useful for unit-testing."""
def __init__(self, hs):
pass
def get_new_events_for_user(self, user, from_token, limit):
return defer.succeed(([], from_token))
def get_current_token_part(self):
return defer.succeed(0)
def get_pagination_rows(self, user, pagination_config, key):
return defer.succeed(([], pagination_config.from_token))
class RoomEventSource(object):
def __init__(self, hs):
self.store = hs.get_datastore()
@defer.inlineCallbacks
def get_new_events_for_user(self, user, from_token, limit):
# We just ignore the key for now.
to_key = yield self.get_current_token_part()
events, end_key = yield self.store.get_room_events_stream(
user_id=user.to_string(),
from_key=from_token.events_key,
to_key=to_key,
room_id=None,
limit=limit,
)
end_token = from_token.copy_and_replace("events_key", end_key)
defer.returnValue((events, end_token))
def get_current_token_part(self):
return self.store.get_room_events_max_id()
@defer.inlineCallbacks
def get_pagination_rows(self, user, pagination_config, key):
from_token = pagination_config.from_token
to_token = pagination_config.to_token
limit = pagination_config.limit
direction = pagination_config.direction
to_key = to_token.events_key if to_token else None
events, next_key = yield self.store.paginate_room_events(
room_id=key,
from_key=from_token.events_key,
to_key=to_key,
direction=direction,
limit=limit,
with_feedback=True
)
next_token = from_token.copy_and_replace("events_key", next_key)
defer.returnValue((events, next_token))
class PresenceSource(object):
def __init__(self, hs):
self.hs = hs
self.clock = hs.get_clock()
def get_new_events_for_user(self, user, from_token, limit):
from_key = int(from_token.presence_key)
presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap
# TODO(paul): limit, and filter by visibility
updates = [(k, cachemap[k]) for k in cachemap
if from_key < cachemap[k].serial]
if updates:
clock = self.clock
latest_serial = max([x[1].serial for x in updates])
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
end_token = from_token.copy_and_replace(
"presence_key", latest_serial
)
return ((data, end_token))
else:
end_token = from_token.copy_and_replace(
"presence_key", presence._user_cachemap_latest_serial
)
return (([], end_token))
def get_current_token_part(self):
presence = self.hs.get_handlers().presence_handler
return presence._user_cachemap_latest_serial
def get_pagination_rows(self, user, pagination_config, key):
# TODO (erikj): Does this make sense? Ordering?
from_token = pagination_config.from_token
to_token = pagination_config.to_token
from_key = int(from_token.presence_key)
if to_token:
to_key = int(to_token.presence_key)
else:
to_key = -1
presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap
# TODO(paul): limit, and filter by visibility
updates = [(k, cachemap[k]) for k in cachemap
if to_key < cachemap[k].serial < from_key]
if updates:
clock = self.clock
earliest_serial = max([x[1].serial for x in updates])
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
if to_token:
next_token = to_token
else:
next_token = from_token
next_token = next_token.copy_and_replace(
"presence_key", earliest_serial
)
return ((data, next_token))
else:
if not to_token:
to_token = from_token.copy_and_replace(
"presence_key", 0
)
return (([], to_token))
class EventSources(object):
SOURCE_TYPES = {
"room": RoomEventSource,
"presence": PresenceSource,
}
def __init__(self, hs):
self.sources = {
name: cls(hs)
for name, cls in EventSources.SOURCE_TYPES.items()
}
@staticmethod
def create_token(events_key, presence_key):
return StreamToken(events_key=events_key, presence_key=presence_key)
@defer.inlineCallbacks
def get_current_token(self):
events_key = yield self.sources["room"].get_current_token_part()
presence_key = yield self.sources["presence"].get_current_token_part()
token = EventSources.create_token(events_key, presence_key)
defer.returnValue(token)
class StreamSource(object):
def get_new_events_for_user(self, user, from_token, limit):
raise NotImplementedError("get_new_events_for_user")
def get_current_token_part(self):
raise NotImplementedError("get_current_token_part")
def get_pagination_rows(self, user, pagination_config, key):
raise NotImplementedError("get_rows")

View File

@@ -92,3 +92,36 @@ class RoomAlias(DomainSpecificString):
class RoomID(DomainSpecificString):
"""Structure representing a room id. """
SIGIL = "!"
class StreamToken(
namedtuple(
"Token",
("events_key", "presence_key")
)
):
_SEPARATOR = "_"
@classmethod
def from_string(cls, string):
try:
events_key, presence_key = string.split(cls._SEPARATOR)
return cls(
events_key=events_key,
presence_key=presence_key,
)
except:
raise SynapseError(400, "Invalid Token")
def to_string(self):
return "".join([
str(self.events_key),
self._SEPARATOR,
str(self.presence_key),
])
def copy_and_replace(self, key, new_value):
d = self._asdict()
d[key] = new_value
return StreamToken(**d)

View File

@@ -15,8 +15,11 @@
from inspect import getcallargs
from functools import wraps
import logging
import inspect
import traceback
def log_function(f):
@@ -26,6 +29,7 @@ def log_function(f):
lineno = f.func_code.co_firstlineno
pathname = f.func_code.co_filename
@wraps(f)
def wrapped(*args, **kwargs):
name = f.__module__
logger = logging.getLogger(name)
@@ -63,4 +67,55 @@ def log_function(f):
return f(*args, **kwargs)
wrapped.__name__ = func_name
return wrapped
def trace_function(f):
func_name = f.__name__
linenum = f.func_code.co_firstlineno
pathname = f.func_code.co_filename
def wrapped(*args, **kwargs):
name = f.__module__
logger = logging.getLogger(name)
level = logging.DEBUG
s = inspect.currentframe().f_back
to_print = [
"\t%s:%s %s. Args: args=%s, kwargs=%s" % (
pathname, linenum, func_name, args, kwargs
)
]
while s:
if True or s.f_globals["__name__"].startswith("synapse"):
filename, lineno, function, _, _ = inspect.getframeinfo(s)
args_string = inspect.formatargvalues(*inspect.getargvalues(s))
to_print.append(
"\t%s:%d %s. Args: %s" % (
filename, lineno, function, args_string
)
)
s = s.f_back
msg = "\nTraceback for %s:\n" % (func_name,) + "\n".join(to_print)
record = logging.LogRecord(
name=name,
level=level,
pathname=pathname,
lineno=lineno,
msg=msg,
args=None,
exc_info=None
)
logger.handle(record)
return f(*args, **kwargs)
wrapped.__name__ = func_name
return wrapped

View File

@@ -58,7 +58,7 @@ class FederationTestCase(unittest.TestCase):
self.mock_persistence = Mock(spec=[
"get_current_state_for_context",
"get_pdu",
"persist_pdu",
"persist_event",
"update_min_depth_for_context",
"prep_send_transaction",
"delivered_txn",

View File

@@ -22,11 +22,14 @@ from synapse.api.events.room import (
from synapse.api.constants import Membership
from synapse.handlers.federation import FederationHandler
from synapse.server import HomeServer
from synapse.federation.units import Pdu
from mock import NonCallableMock
from mock import NonCallableMock, ANY
import logging
from ..utils import get_mock_call_args
logging.getLogger().addHandler(logging.NullHandler())
@@ -60,46 +63,53 @@ class FederationTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_msg(self):
event = self.hs.get_event_factory().create_event(
etype=MessageEvent.TYPE,
msg_id="bob",
room_id="foo",
pdu = Pdu(
pdu_type=MessageEvent.TYPE,
context="foo",
content={"msgtype": u"fooo"},
ts=0,
pdu_id="a",
origin="b",
)
store_id = "ASD"
self.datastore.persist_event.return_value = defer.succeed(store_id)
self.datastore.get_room.return_value = defer.succeed(True)
yield self.handlers.federation_handler.on_receive(event, False, False)
yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
self.datastore.persist_event.assert_called_once_with(event, False)
self.notifier.on_new_room_event.assert_called_once_with(
event, store_id)
self.datastore.persist_event.assert_called_once_with(ANY, False)
self.notifier.on_new_room_event.assert_called_once_with(ANY)
@defer.inlineCallbacks
def test_invite_join_target_this(self):
room_id = "foo"
user_id = "@bob:red"
event = self.hs.get_event_factory().create_event(
etype=InviteJoinEvent.TYPE,
pdu = Pdu(
pdu_type=InviteJoinEvent.TYPE,
user_id=user_id,
target_host=self.hostname,
room_id=room_id,
context=room_id,
content={},
ts=0,
pdu_id="a",
origin="b",
)
yield self.handlers.federation_handler.on_receive(event, False, False)
yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
mem_handler = self.handlers.room_member_handler
self.assertEquals(1, mem_handler.change_membership.call_count)
self.assertEquals(True, mem_handler.change_membership.call_args[0][1])
call_args = get_mock_call_args(
lambda event, do_auth: None,
mem_handler.change_membership
)
self.assertEquals(True, call_args["do_auth"])
new_event = mem_handler.change_membership.call_args[0][0]
new_event = call_args["event"]
self.assertEquals(RoomMemberEvent.TYPE, new_event.type)
self.assertEquals(room_id, new_event.room_id)
self.assertEquals(user_id, new_event.target_user_id)
self.assertEquals(user_id, new_event.state_key)
self.assertEquals(Membership.JOIN, new_event.membership)
@@ -108,15 +118,18 @@ class FederationTestCase(unittest.TestCase):
room_id = "foo"
user_id = "@bob:red"
event = self.hs.get_event_factory().create_event(
etype=InviteJoinEvent.TYPE,
pdu = Pdu(
pdu_type=InviteJoinEvent.TYPE,
user_id=user_id,
target_user_id="@red:not%s" % self.hostname,
room_id=room_id,
state_key="@red:not%s" % self.hostname,
context=room_id,
content={},
ts=0,
pdu_id="a",
origin="b",
)
yield self.handlers.federation_handler.on_receive(event, False, False)
yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
mem_handler = self.handlers.room_member_handler
self.assertEquals(0, mem_handler.change_membership.call_count)

View File

@@ -15,7 +15,7 @@
from twisted.trial import unittest
from twisted.internet import defer
from twisted.internet import defer, reactor
from mock import Mock, call, ANY
import logging
@@ -92,10 +92,7 @@ class PresenceStateTestCase(unittest.TestCase):
self.datastore.is_presence_visible = is_presence_visible
# Mock the RoomMemberHandler
room_member_handler = Mock(spec=[
"get_rooms_for_user",
"get_room_members",
])
room_member_handler = Mock(spec=[])
hs.handlers.room_member_handler = room_member_handler
logging.getLogger().debug("Mocking room_member_handler=%r", room_member_handler)
@@ -122,6 +119,11 @@ class PresenceStateTestCase(unittest.TestCase):
return defer.succeed([])
room_member_handler.get_room_members = get_room_members
def do_users_share_a_room(userlist):
shared = all(map(lambda u: u in self.room_members, userlist))
return defer.succeed(shared)
self.datastore.do_users_share_a_room = do_users_share_a_room
self.mock_start = Mock()
self.mock_stop = Mock()
@@ -190,7 +192,8 @@ class PresenceStateTestCase(unittest.TestCase):
),
SynapseError
)
test_get_disallowed_state.skip = "Presence polling is disabled"
test_get_disallowed_state.skip = "Presence permissions are disabled"
@defer.inlineCallbacks
def test_set_my_state(self):
@@ -215,7 +218,6 @@ class PresenceStateTestCase(unittest.TestCase):
state={"state": OFFLINE})
self.mock_stop.assert_called_with(self.u_apple)
test_set_my_state.skip = "Presence polling is disabled"
class PresenceInvitesTestCase(unittest.TestCase):
@@ -497,6 +499,7 @@ class PresencePushTestCase(unittest.TestCase):
db_pool=None,
datastore=Mock(spec=[
"set_presence_state",
"get_joined_hosts_for_room",
# Bits that Federation needs
"prep_send_transaction",
@@ -511,8 +514,12 @@ class PresencePushTestCase(unittest.TestCase):
)
hs.handlers = JustPresenceHandlers(hs)
def update(*args,**kwargs):
# print "mock_update_client: Args=%s, kwargs=%s" %(args, kwargs,)
return defer.succeed(None)
self.mock_update_client = Mock()
self.mock_update_client.return_value = defer.succeed(None)
self.mock_update_client.side_effect = update
self.datastore = hs.get_datastore()
@@ -546,6 +553,14 @@ class PresencePushTestCase(unittest.TestCase):
return defer.succeed([])
self.room_member_handler.get_room_members = get_room_members
def get_room_hosts(room_id):
if room_id == "a-room":
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
@defer.inlineCallbacks
def fetch_room_distributions_into(room_id, localusers=None,
remotedomains=None, ignore_user=None):
@@ -611,18 +626,10 @@ class PresencePushTestCase(unittest.TestCase):
{"state": ONLINE})
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_apple,
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
room_ids=["a-room"],
observed_user=self.u_apple,
statuscache=ANY), # self-reflection
call(observer_user=self.u_banana,
observed_user=self.u_apple,
statuscache=ANY),
call(observer_user=self.u_clementine,
observed_user=self.u_apple,
statuscache=ANY),
call(observer_user=self.u_elderberry,
observed_user=self.u_apple,
statuscache=ANY),
], any_order=True)
self.mock_update_client.reset_mock()
@@ -651,30 +658,30 @@ class PresencePushTestCase(unittest.TestCase):
], presence)
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_banana,
call(users_to_push=set([self.u_banana]),
room_ids=[],
observed_user=self.u_banana,
statuscache=ANY), # self-reflection
]) # and no others...
test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_push_remote(self):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("remote",
path=ANY, # Can't guarantee which txn ID will be which
data=_expect_edu("remote", "m.presence",
content={
"push": [
{"user_id": "@apple:test",
"state": "online",
"mtime_age": 0},
],
}
)
),
defer.succeed((200, "OK"))
)
# put_json.expect_call_and_return(
# call("remote",
# path=ANY, # Can't guarantee which txn ID will be which
# data=_expect_edu("remote", "m.presence",
# content={
# "push": [
# {"user_id": "@apple:test",
# "state": "online",
# "mtime_age": 0},
# ],
# }
# )
# ),
# defer.succeed((200, "OK"))
# )
put_json.expect_call_and_return(
call("farm",
path=ANY, # Can't guarantee which txn ID will be which
@@ -682,7 +689,7 @@ class PresencePushTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@apple:test",
"state": "online",
"state": u"online",
"mtime_age": 0},
],
}
@@ -707,7 +714,6 @@ class PresencePushTestCase(unittest.TestCase):
)
yield put_json.await_calls()
test_push_remote.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_recv_remote(self):
@@ -732,10 +738,8 @@ class PresencePushTestCase(unittest.TestCase):
)
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_apple,
observed_user=self.u_potato,
statuscache=ANY),
call(observer_user=self.u_banana,
call(users_to_push=set([self.u_apple]),
room_ids=["a-room"],
observed_user=self.u_potato,
statuscache=ANY),
], any_order=True)
@@ -755,19 +759,17 @@ class PresencePushTestCase(unittest.TestCase):
)
self.mock_update_client.assert_has_calls([
# Apple and Elderberry see each other
call(observer_user=self.u_apple,
call(room_ids=["a-room"],
observed_user=self.u_elderberry,
users_to_push=set(),
statuscache=ANY),
call(observer_user=self.u_elderberry,
call(users_to_push=set([self.u_elderberry]),
observed_user=self.u_apple,
room_ids=[],
statuscache=ANY),
# Banana and Elderberry see each other
call(observer_user=self.u_banana,
observed_user=self.u_elderberry,
statuscache=ANY),
call(observer_user=self.u_elderberry,
call(users_to_push=set([self.u_elderberry]),
observed_user=self.u_banana,
room_ids=[],
statuscache=ANY),
], any_order=True)
@@ -855,6 +857,7 @@ class PresencePollingTestCase(unittest.TestCase):
'apple': [ "@banana:test", "@clementine:test" ],
'banana': [ "@apple:test" ],
'clementine': [ "@apple:test", "@potato:remote" ],
'fig': [ "@potato:remote" ],
}
@@ -888,7 +891,12 @@ class PresencePollingTestCase(unittest.TestCase):
self.datastore.get_received_txn_response = get_received_txn_response
self.mock_update_client = Mock()
self.mock_update_client.return_value = defer.succeed(None)
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
@@ -904,9 +912,10 @@ class PresencePollingTestCase(unittest.TestCase):
# Mocked database state
# Local users always start offline
self.current_user_state = {
"apple": OFFLINE,
"banana": OFFLINE,
"clementine": OFFLINE,
"apple": OFFLINE,
"banana": OFFLINE,
"clementine": OFFLINE,
"fig": OFFLINE,
}
def get_presence_state(user_localpart):
@@ -936,6 +945,7 @@ class PresencePollingTestCase(unittest.TestCase):
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")
@@ -950,10 +960,10 @@ class PresencePollingTestCase(unittest.TestCase):
# apple should see both banana and clementine currently offline
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_apple,
call(users_to_push=[self.u_apple],
observed_user=self.u_banana,
statuscache=ANY),
call(observer_user=self.u_apple,
call(users_to_push=[self.u_apple],
observed_user=self.u_clementine,
statuscache=ANY),
], any_order=True)
@@ -973,10 +983,11 @@ class PresencePollingTestCase(unittest.TestCase):
# apple and banana should now both see each other online
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_apple,
call(users_to_push=set([self.u_apple]),
observed_user=self.u_banana,
room_ids=[],
statuscache=ANY),
call(observer_user=self.u_banana,
call(users_to_push=[self.u_banana],
observed_user=self.u_apple,
statuscache=ANY),
], any_order=True)
@@ -993,14 +1004,14 @@ class PresencePollingTestCase(unittest.TestCase):
# banana should now be told apple is offline
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_banana,
call(users_to_push=set([self.u_banana, self.u_apple]),
observed_user=self.u_apple,
room_ids=[],
statuscache=ANY),
], any_order=True)
self.assertFalse("banana" in self.handler._local_pushmap)
self.assertFalse("clementine" in self.handler._local_pushmap)
test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks
@@ -1008,7 +1019,7 @@ class PresencePollingTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("remote",
path="/matrix/federation/v1/send/1000000/",
path=ANY,
data=_expect_edu("remote", "m.presence",
content={
"poll": [ "@potato:remote" ],
@@ -1018,6 +1029,18 @@ class PresencePollingTestCase(unittest.TestCase):
defer.succeed((200, "OK"))
)
put_json.expect_call_and_return(
call("remote",
path=ANY,
data=_expect_edu("remote", "m.presence",
content={
"push": [ {"user_id": "@clementine:test" }],
},
),
),
defer.succeed((200, "OK"))
)
# clementine goes online
yield self.handler.set_state(
target_user=self.u_clementine, auth_user=self.u_clementine,
@@ -1026,13 +1049,48 @@ class PresencePollingTestCase(unittest.TestCase):
yield put_json.await_calls()
# Gut-wrenching tests
self.assertTrue(self.u_potato in self.handler._remote_recvmap)
self.assertTrue(self.u_potato in self.handler._remote_recvmap,
msg="expected potato to be in _remote_recvmap"
)
self.assertTrue(self.u_clementine in
self.handler._remote_recvmap[self.u_potato])
put_json.expect_call_and_return(
call("remote",
path="/matrix/federation/v1/send/1000001/",
path=ANY,
data=_expect_edu("remote", "m.presence",
content={
"push": [ {"user_id": "@fig:test" }],
},
),
),
defer.succeed((200, "OK"))
)
# fig goes online; shouldn't send a second poll
yield self.handler.set_state(
target_user=self.u_fig, auth_user=self.u_fig,
state={"state": ONLINE}
)
# reactor.iterate(delay=0)
yield put_json.await_calls()
# fig goes offline
yield self.handler.set_state(
target_user=self.u_fig, auth_user=self.u_fig,
state={"state": OFFLINE}
)
reactor.iterate(delay=0)
put_json.assert_had_no_calls()
put_json.expect_call_and_return(
call("remote",
path=ANY,
data=_expect_edu("remote", "m.presence",
content={
"unpoll": [ "@potato:remote" ],
@@ -1047,10 +1105,11 @@ class PresencePollingTestCase(unittest.TestCase):
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"state": OFFLINE})
put_json.await_calls()
yield put_json.await_calls()
self.assertFalse(self.u_potato in self.handler._remote_recvmap)
test_remote_poll_send.skip = "Presence polling is disabled"
self.assertFalse(self.u_potato in self.handler._remote_recvmap,
msg="expected potato not to be in _remote_recvmap"
)
@defer.inlineCallbacks
def test_remote_poll_receive(self):

View File

@@ -81,7 +81,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
self.replication = hs.get_replication_layer()
self.replication.send_edu = Mock()
self.replication.send_edu.return_value = defer.succeed((200, "OK"))
def send_edu(*args, **kwargs):
# print "send_edu: %s, %s" % (args, kwargs)
return defer.succeed((200, "OK"))
self.replication.send_edu.side_effect = send_edu
def get_profile_displayname(user_localpart):
return defer.succeed("Frank")
@@ -95,17 +99,25 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
return defer.succeed("http://foo")
self.datastore.get_profile_avatar_url = get_profile_avatar_url
self.presence_list = [
{"observed_user_id": "@banana:test"},
{"observed_user_id": "@clementine:test"},
]
def get_presence_list(user_localpart, accepted=None):
return defer.succeed([
{"observed_user_id": "@banana:test"},
{"observed_user_id": "@clementine:test"},
])
return defer.succeed(self.presence_list)
self.datastore.get_presence_list = get_presence_list
def do_users_share_a_room(userlist):
return defer.succeed(False)
self.datastore.do_users_share_a_room = do_users_share_a_room
self.handlers = hs.get_handlers()
self.mock_update_client = Mock()
self.mock_update_client.return_value = defer.succeed(None)
def update(*args, **kwargs):
# print "mock_update_client: %s, %s" %(args, kwargs)
return defer.succeed(None)
self.mock_update_client.side_effect = update
self.handlers.presence_handler.push_update_to_clients = (
self.mock_update_client)
@@ -126,6 +138,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_set_my_state(self):
self.presence_list = [
{"observed_user_id": "@banana:test"},
{"observed_user_id": "@clementine:test"},
]
mocked_set = self.datastore.set_presence_state
mocked_set.return_value = defer.succeed({"state": OFFLINE})
@@ -135,10 +152,14 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
mocked_set.assert_called_with("apple",
{"state": UNAVAILABLE, "status_msg": "Away"})
test_set_my_state.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_push_local(self):
self.presence_list = [
{"observed_user_id": "@banana:test"},
{"observed_user_id": "@clementine:test"},
]
self.datastore.set_presence_state.return_value = defer.succeed(
{"state": ONLINE})
@@ -170,12 +191,10 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
presence)
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_apple,
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
room_ids=[],
observed_user=self.u_apple,
statuscache=ANY), # self-reflection
call(observer_user=self.u_banana,
observed_user=self.u_apple,
statuscache=ANY),
], any_order=True)
statuscache = self.mock_update_client.call_args[1]["statuscache"]
@@ -195,12 +214,10 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
self.u_apple, "I am an Apple")
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_apple,
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
room_ids=[],
observed_user=self.u_apple,
statuscache=ANY), # self-reflection
call(observer_user=self.u_banana,
observed_user=self.u_apple,
statuscache=ANY),
], any_order=True)
statuscache = self.mock_update_client.call_args[1]["statuscache"]
@@ -210,11 +227,14 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
"displayname": "I am an Apple",
"avatar_url": "http://foo",
}, statuscache.state)
test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_push_remote(self):
self.presence_list = [
{"observed_user_id": "@potato:remote"},
]
self.datastore.set_presence_state.return_value = defer.succeed(
{"state": ONLINE})
@@ -242,10 +262,14 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
],
},
)
test_push_remote.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_recv_remote(self):
self.presence_list = [
{"observed_user_id": "@banana:test"},
{"observed_user_id": "@clementine:test"},
]
# TODO(paul): Gut-wrenching
potato_set = self.handlers.presence_handler._remote_recvmap.setdefault(
self.u_potato, set())
@@ -263,7 +287,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
)
self.mock_update_client.assert_called_with(
observer_user=self.u_apple,
users_to_push=set([self.u_apple]),
room_ids=[],
observed_user=self.u_potato,
statuscache=ANY)

View File

@@ -45,6 +45,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
"get_room_member",
"get_room",
"store_room",
"snapshot_room",
]),
resource_for_federation=NonCallableMock(),
http_client=NonCallableMock(spec_set=[]),
@@ -52,29 +53,36 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
handlers=NonCallableMock(spec_set=[
"room_member_handler",
"profile_handler",
"federation_handler",
]),
auth=NonCallableMock(spec_set=["check"]),
federation=NonCallableMock(spec_set=[
"handle_new_event",
"get_state_for_room",
]),
state_handler=NonCallableMock(spec_set=["handle_new_event"]),
)
self.federation = NonCallableMock(spec_set=[
"handle_new_event",
"get_state_for_room",
])
self.datastore = hs.get_datastore()
self.handlers = hs.get_handlers()
self.notifier = hs.get_notifier()
self.federation = hs.get_federation()
self.state_handler = hs.get_state_handler()
self.distributor = hs.get_distributor()
self.hs = hs
self.handlers.federation_handler = self.federation
self.distributor.declare("collect_presencelike_data")
self.handlers.room_member_handler = RoomMemberHandler(self.hs)
self.handlers.profile_handler = ProfileHandler(self.hs)
self.room_member_handler = self.handlers.room_member_handler
self.snapshot = Mock()
self.datastore.snapshot_room.return_value = self.snapshot
@defer.inlineCallbacks
def test_invite(self):
room_id = "!foo:red"
@@ -85,7 +93,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
event = self.hs.get_event_factory().create_event(
etype=RoomMemberEvent.TYPE,
user_id=user_id,
target_user_id=target_user_id,
state_key=target_user_id,
room_id=room_id,
membership=Membership.INVITE,
content=content,
@@ -104,8 +112,12 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
# Actual invocation
yield self.room_member_handler.change_membership(event)
self.state_handler.handle_new_event.assert_called_once_with(event)
self.federation.handle_new_event.assert_called_once_with(event)
self.state_handler.handle_new_event.assert_called_once_with(
event, self.snapshot,
)
self.federation.handle_new_event.assert_called_once_with(
event, self.snapshot,
)
self.assertEquals(
set(["blue", "red", "green"]),
@@ -116,8 +128,8 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
event
)
self.notifier.on_new_room_event.assert_called_once_with(
event, store_id)
event, extra_users=[self.hs.parse_userid(target_user_id)]
)
self.assertFalse(self.datastore.get_room.called)
self.assertFalse(self.datastore.store_room.called)
self.assertFalse(self.federation.get_state_for_room.called)
@@ -133,7 +145,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
event = self.hs.get_event_factory().create_event(
etype=RoomMemberEvent.TYPE,
user_id=user_id,
target_user_id=target_user_id,
state_key=target_user_id,
room_id=room_id,
membership=Membership.JOIN,
content=content,
@@ -148,6 +160,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
self.datastore.get_joined_hosts_for_room.side_effect = get_joined
store_id = "store_id_fooo"
self.datastore.persist_event.return_value = defer.succeed(store_id)
self.datastore.get_room.return_value = defer.succeed(1) # Not None.
@@ -163,8 +176,12 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
# Actual invocation
yield self.room_member_handler.change_membership(event)
self.state_handler.handle_new_event.assert_called_once_with(event)
self.federation.handle_new_event.assert_called_once_with(event)
self.state_handler.handle_new_event.assert_called_once_with(
event, self.snapshot
)
self.federation.handle_new_event.assert_called_once_with(
event, self.snapshot
)
self.assertEquals(
set(["red", "green"]),
@@ -175,7 +192,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
event
)
self.notifier.on_new_room_event.assert_called_once_with(
event, store_id)
event, extra_users=[user])
join_signal_observer.assert_called_with(
user=user, room_id=room_id)
@@ -312,27 +329,31 @@ class RoomCreationTest(unittest.TestCase):
db_pool=None,
datastore=NonCallableMock(spec_set=[
"store_room",
"snapshot_room",
]),
http_client=NonCallableMock(spec_set=[]),
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
handlers=NonCallableMock(spec_set=[
"room_creation_handler",
"room_member_handler",
"federation_handler",
]),
auth=NonCallableMock(spec_set=["check"]),
federation=NonCallableMock(spec_set=[
"handle_new_event",
]),
state_handler=NonCallableMock(spec_set=["handle_new_event"]),
)
self.federation = NonCallableMock(spec_set=[
"handle_new_event",
])
self.datastore = hs.get_datastore()
self.handlers = hs.get_handlers()
self.notifier = hs.get_notifier()
self.federation = hs.get_federation()
self.state_handler = hs.get_state_handler()
self.hs = hs
self.handlers.federation_handler = self.federation
self.handlers.room_creation_handler = RoomCreationHandler(self.hs)
self.room_creation_handler = self.handlers.room_creation_handler
@@ -359,7 +380,7 @@ class RoomCreationTest(unittest.TestCase):
self.assertEquals(RoomMemberEvent.TYPE, join_event.type)
self.assertEquals(room_id, join_event.room_id)
self.assertEquals(user_id, join_event.user_id)
self.assertEquals(user_id, join_event.target_user_id)
self.assertEquals(user_id, join_event.state_key)
self.assertTrue(self.state_handler.handle_new_event.called)

View File

@@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.trial import unittest
from twisted.internet import defer
from mock import Mock, call, ANY
import json
import logging
from ..utils import MockHttpResource, MockClock, DeferredMockCallable
from synapse.server import HomeServer
from synapse.handlers.typing import TypingNotificationHandler
logging.getLogger().addHandler(logging.NullHandler())
def _expect_edu(destination, edu_type, content, origin="test"):
return {
"origin": origin,
"ts": 1000000,
"pdus": [],
"edus": [
{
"origin": origin,
"destination": destination,
"edu_type": edu_type,
"content": content,
}
],
}
def _make_edu_json(origin, edu_type, content):
return json.dumps(_expect_edu("test", edu_type, content, origin=origin))
class JustTypingNotificationHandlers(object):
def __init__(self, hs):
self.typing_notification_handler = TypingNotificationHandler(hs)
class TypingNotificationsTestCase(unittest.TestCase):
"""Tests typing notifications to rooms."""
def setUp(self):
self.clock = MockClock()
self.mock_http_client = Mock(spec=[])
self.mock_http_client.put_json = DeferredMockCallable()
self.mock_federation_resource = MockHttpResource()
hs = HomeServer("test",
clock=self.clock,
db_pool=None,
datastore=Mock(spec=[
# Bits that Federation needs
"prep_send_transaction",
"delivered_txn",
"get_received_txn_response",
"set_received_txn_response",
]),
handlers=None,
resource_for_client=Mock(),
resource_for_federation=self.mock_federation_resource,
http_client=self.mock_http_client,
)
hs.handlers = JustTypingNotificationHandlers(hs)
self.mock_update_client = Mock()
self.mock_update_client.return_value = defer.succeed(None)
self.handler = hs.get_handlers().typing_notification_handler
self.handler.push_update_to_clients = self.mock_update_client
self.datastore = hs.get_datastore()
def get_received_txn_response(*args):
return defer.succeed(None)
self.datastore.get_received_txn_response = get_received_txn_response
self.room_id = "a-room"
# Mock the RoomMemberHandler
hs.handlers.room_member_handler = Mock(spec=[])
self.room_member_handler = hs.handlers.room_member_handler
self.room_members = []
def get_rooms_for_user(user):
if user in self.room_members:
return defer.succeed([self.room_id])
else:
return defer.succeed([])
self.room_member_handler.get_rooms_for_user = get_rooms_for_user
def get_room_members(room_id):
if room_id == self.room_id:
return defer.succeed(self.room_members)
else:
return defer.succeed([])
self.room_member_handler.get_room_members = get_room_members
@defer.inlineCallbacks
def fetch_room_distributions_into(room_id, localusers=None,
remotedomains=None, ignore_user=None):
members = yield get_room_members(room_id)
for member in members:
if ignore_user is not None and member == ignore_user:
continue
if member.is_mine:
if localusers is not None:
localusers.add(member)
else:
if remotedomains is not None:
remotedomains.add(member.domain)
self.room_member_handler.fetch_room_distributions_into = (
fetch_room_distributions_into)
# Some local users to test with
self.u_apple = hs.parse_userid("@apple:test")
self.u_banana = hs.parse_userid("@banana:test")
# Remote user
self.u_onion = hs.parse_userid("@onion:farm")
@defer.inlineCallbacks
def test_started_typing_local(self):
self.room_members = [self.u_apple, self.u_banana]
yield self.handler.started_typing(
target_user=self.u_apple,
auth_user=self.u_apple,
room_id=self.room_id,
timeout=20000,
)
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_banana,
observed_user=self.u_apple,
room_id=self.room_id,
typing=True),
])
@defer.inlineCallbacks
def test_started_typing_remote_send(self):
self.room_members = [self.u_apple, self.u_onion]
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("farm",
path="/matrix/federation/v1/send/1000000/",
data=_expect_edu("farm", "m.typing",
content={
"room_id": self.room_id,
"user_id": self.u_apple.to_string(),
"typing": True,
}
)
),
defer.succeed((200, "OK"))
)
yield self.handler.started_typing(
target_user=self.u_apple,
auth_user=self.u_apple,
room_id=self.room_id,
timeout=20000,
)
yield put_json.await_calls()
@defer.inlineCallbacks
def test_started_typing_remote_recv(self):
self.room_members = [self.u_apple, self.u_onion]
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
_make_edu_json("farm", "m.typing",
content={
"room_id": self.room_id,
"user_id": self.u_onion.to_string(),
"typing": True,
}
)
)
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_apple,
observed_user=self.u_onion,
room_id=self.room_id,
typing=True),
])
@defer.inlineCallbacks
def test_stopped_typing(self):
self.room_members = [self.u_apple, self.u_banana, self.u_onion]
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("farm",
path="/matrix/federation/v1/send/1000000/",
data=_expect_edu("farm", "m.typing",
content={
"room_id": self.room_id,
"user_id": self.u_apple.to_string(),
"typing": False,
}
)
),
defer.succeed((200, "OK"))
)
# Gut-wrenching
from synapse.handlers.typing import RoomMember
self.handler._member_typing_until[
RoomMember(self.room_id, self.u_apple)
] = 1002000
yield self.handler.stopped_typing(
target_user=self.u_apple,
auth_user=self.u_apple,
room_id=self.room_id,
)
self.mock_update_client.assert_has_calls([
call(observer_user=self.u_banana,
observed_user=self.u_apple,
room_id=self.room_id,
typing=False),
])
yield put_json.await_calls()

View File

@@ -128,9 +128,9 @@ class EventStreamPermissionsTestCase(RestTestCase):
"test",
db_pool=None,
http_client=None,
federation=Mock(),
replication_layer=Mock(),
state_handler=state_handler,
datastore=MemoryDataStore(),
persistence_service=persistence_service,
clock=Mock(spec=[
"call_later",
@@ -139,9 +139,10 @@ class EventStreamPermissionsTestCase(RestTestCase):
]),
)
hs.get_handlers().federation_handler = Mock()
hs.get_clock().time_msec.return_value = 1000000
hs.datastore = MemoryDataStore()
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)
@@ -178,10 +179,9 @@ class EventStreamPermissionsTestCase(RestTestCase):
@defer.inlineCallbacks
def test_stream_room_permissions(self):
room_id = "!rid1:test"
yield self.create_room_as(room_id, self.other_user,
tok=self.other_token)
yield self.send(room_id, self.other_user, tok=self.other_token)
room_id = yield self.create_room_as(self.other_user,
tok=self.other_token)
yield self.send(room_id, tok=self.other_token)
# invited to room (expect no content for room)
yield self.invite(room_id, src=self.other_user, targ=self.user_id,

View File

@@ -114,7 +114,6 @@ class PresenceStateTestCase(unittest.TestCase):
self.assertEquals(200, code)
mocked_set.assert_called_with("apple",
{"state": UNAVAILABLE, "status_msg": "Away"})
test_set_my_status.skip = "Presence polling is disabled"
class PresenceListTestCase(unittest.TestCase):
@@ -171,7 +170,7 @@ class PresenceListTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("GET",
"/presence_list/%s" % (myid), None)
"/presence/list/%s" % (myid), None)
self.assertEquals(200, code)
self.assertEquals(
@@ -192,7 +191,7 @@ class PresenceListTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("POST",
"/presence_list/%s" % (myid),
"/presence/list/%s" % (myid),
"""{"invite": ["@banana:test"]}"""
)
@@ -212,7 +211,7 @@ class PresenceListTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("POST",
"/presence_list/%s" % (myid),
"/presence/list/%s" % (myid),
"""{"drop": ["@banana:test"]}"""
)
@@ -229,11 +228,19 @@ class PresenceEventStreamTestCase(unittest.TestCase):
# HIDEOUS HACKERY
# TODO(paul): This should be injected in via the HomeServer DI system
from synapse.handlers.events import EventStreamHandler
from synapse.handlers.presence import PresenceStreamData
EventStreamHandler.stream_data_classes = [
PresenceStreamData
]
from synapse.streams.events import (
PresenceSource, 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"] = PresenceSource
hs = HomeServer("test",
db_pool=None,
@@ -288,7 +295,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
# all be ours
# I'll already get my own presence state change
self.assertEquals({"start": "1", "end": "1", "chunk": []}, response)
self.assertEquals({"start": "0_1", "end": "0_1", "chunk": []}, response)
self.mock_datastore.set_presence_state.return_value = defer.succeed(
{"state": ONLINE})
@@ -299,10 +306,10 @@ class PresenceEventStreamTestCase(unittest.TestCase):
state={"state": ONLINE})
(code, response) = yield self.mock_resource.trigger("GET",
"/events?from=1&timeout=0", None)
"/events?from=0_1&timeout=0", None)
self.assertEquals(200, code)
self.assertEquals({"start": "1", "end": "2", "chunk": [
self.assertEquals({"start": "0_1", "end": "0_2", "chunk": [
{"type": "m.presence",
"content": {
"user_id": "@banana:test",
@@ -310,4 +317,3 @@ class PresenceEventStreamTestCase(unittest.TestCase):
"mtime_age": 0,
}},
]}, response)
test_shortpoll.skip = "Presence polling is disabled"

View File

@@ -54,12 +54,12 @@ class RoomPermissionsTestCase(RestTestCase):
"test",
db_pool=None,
http_client=None,
federation=Mock(),
datastore=MemoryDataStore(),
replication_layer=Mock(),
state_handler=state_handler,
persistence_service=persistence_service,
)
hs.get_handlers().federation_handler = Mock()
def _get_user_by_token(token=None):
return hs.parse_userid(self.auth_user_id)
@@ -74,17 +74,15 @@ class RoomPermissionsTestCase(RestTestCase):
# create some rooms under the name rmcreator_id
self.uncreated_rmid = "!aa:test"
self.created_rmid = "!abc:test"
yield self.create_room_as(self.created_rmid, self.rmcreator_id,
is_public=False)
self.created_rmid = yield self.create_room_as(self.rmcreator_id,
is_public=False)
self.created_public_rmid = "!def1234ghi:test"
yield self.create_room_as(self.created_public_rmid, self.rmcreator_id,
is_public=True)
self.created_public_rmid = yield self.create_room_as(self.rmcreator_id,
is_public=True)
# send a message in one of the rooms
self.created_rmid_msg_path = ("/rooms/%s/messages/%s/midaaa1" %
(self.created_rmid, self.rmcreator_id))
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,
@@ -94,7 +92,7 @@ class RoomPermissionsTestCase(RestTestCase):
# set topic for public room
(code, response) = yield self.mock_resource.trigger(
"PUT",
"/rooms/%s/topic" % self.created_public_rmid,
"/rooms/%s/state/m.room.topic" % self.created_public_rmid,
'{"topic":"Public Room Topic"}')
self.assertEquals(200, code, msg=str(response))
@@ -138,14 +136,14 @@ class RoomPermissionsTestCase(RestTestCase):
@defer.inlineCallbacks
def test_send_message(self):
msg_content = '{"msgtype":"m.text","body":"hello"}'
send_msg_path = ("/rooms/%s/messages/%s/mid1" %
(self.created_rmid, self.user_id))
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/messages/%s/mid1" %
(self.uncreated_rmid, self.user_id), msg_content)
"/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
@@ -175,15 +173,15 @@ class RoomPermissionsTestCase(RestTestCase):
@defer.inlineCallbacks
def test_topic_perms(self):
topic_content = '{"topic":"My Topic Name"}'
topic_path = "/rooms/%s/topic" % self.created_rmid
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/topic" % self.uncreated_rmid,
"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/topic" % self.uncreated_rmid)
"/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
@@ -223,19 +221,19 @@ class RoomPermissionsTestCase(RestTestCase):
# get topic in PUBLIC room, not joined, expect 200 (or 404)
(code, response) = yield self.mock_resource.trigger_get(
"/rooms/%s/topic" % self.created_public_rmid)
"/rooms/%s/state/m.room.topic" % self.created_public_rmid)
self.assertEquals(200, code, msg=str(response))
# set topic in PUBLIC room, not joined, expect 403
(code, response) = yield self.mock_resource.trigger(
"PUT",
"/rooms/%s/topic" % self.created_public_rmid,
"/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/members/%s/state"
path = "/rooms/%s/state/m.room.member/%s"
for member in members:
(code, response) = yield self.mock_resource.trigger_get(
path %
@@ -291,12 +289,12 @@ class RoomPermissionsTestCase(RestTestCase):
def test_membership_public_room_perms(self):
room = self.created_public_rmid
# get membership of self, get membership of other, public room + invite
# expect all 403s
# expect all 200s - public rooms, you can see who is in them.
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)
room=room, expect_code=200)
# get membership of self, get membership of other, public room + joined
# expect all 200s
@@ -306,11 +304,11 @@ class RoomPermissionsTestCase(RestTestCase):
room=room, expect_code=200)
# get membership of self, get membership of other, public room + left
# expect all 403s
# expect all 200s - public rooms, you can always see who is in them.
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)
room=room, expect_code=200)
@defer.inlineCallbacks
def test_invited_permissions(self):
@@ -403,12 +401,12 @@ class RoomsMemberListTestCase(RestTestCase):
"test",
db_pool=None,
http_client=None,
federation=Mock(),
datastore=MemoryDataStore(),
replication_layer=Mock(),
state_handler=state_handler,
persistence_service=persistence_service,
)
hs.get_handlers().federation_handler = Mock()
self.auth_user_id = self.user_id
@@ -423,32 +421,29 @@ class RoomsMemberListTestCase(RestTestCase):
@defer.inlineCallbacks
def test_get_member_list(self):
room_id = "!aa:test"
yield self.create_room_as(room_id, self.user_id)
room_id = yield self.create_room_as(self.user_id)
(code, response) = yield self.mock_resource.trigger_get(
"/rooms/%s/members/list" % room_id)
"/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/list")
"/rooms/roomdoesnotexist/members")
self.assertEquals(403, code, msg=str(response))
@defer.inlineCallbacks
def test_get_member_list_no_permission(self):
room_id = "!bb:test"
yield self.create_room_as(room_id, "@some_other_guy:red")
room_id = yield self.create_room_as("@some_other_guy:red")
(code, response) = yield self.mock_resource.trigger_get(
"/rooms/%s/members/list" % room_id)
"/rooms/%s/members" % room_id)
self.assertEquals(403, code, msg=str(response))
@defer.inlineCallbacks
def test_get_member_list_mixed_memberships(self):
room_id = "!bb:test"
room_creator = "@some_other_guy:blue"
room_path = "/rooms/%s/members/list" % room_id
yield self.create_room_as(room_id, room_creator)
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.
@@ -484,12 +479,12 @@ class RoomsCreateTestCase(RestTestCase):
"test",
db_pool=None,
http_client=None,
federation=Mock(),
datastore=MemoryDataStore(),
replication_layer=Mock(),
state_handler=state_handler,
persistence_service=persistence_service,
)
hs.get_handlers().federation_handler = Mock()
def _get_user_by_token(token=None):
return hs.parse_userid(self.auth_user_id)
@@ -503,107 +498,57 @@ class RoomsCreateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_post_room_no_keys(self):
# POST with no config keys, expect new room id
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
"{}")
(code, response) = yield self.mock_resource.trigger("POST",
"/createRoom",
"{}")
self.assertEquals(200, code, response)
self.assertTrue("room_id" in response)
@defer.inlineCallbacks
def test_post_room_visibility_key(self):
# POST with visibility config key, expect new room id
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'{"visibility":"private"}')
(code, response) = yield self.mock_resource.trigger(
"POST",
"/createRoom",
'{"visibility":"private"}')
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@defer.inlineCallbacks
def test_post_room_custom_key(self):
# POST with custom config keys, expect new room id
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'{"custom":"stuff"}')
(code, response) = yield self.mock_resource.trigger(
"POST",
"/createRoom",
'{"custom":"stuff"}')
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@defer.inlineCallbacks
def test_post_room_known_and_unknown_keys(self):
# POST with custom + known config keys, expect new room id
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'{"visibility":"private","custom":"things"}')
(code, response) = yield self.mock_resource.trigger(
"POST",
"/createRoom",
'{"visibility":"private","custom":"things"}')
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@defer.inlineCallbacks
def test_post_room_invalid_content(self):
# POST with invalid content / paths, expect 400
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'{"visibili')
self.assertEquals(400, code)
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
'["hello"]')
self.assertEquals(400, code)
@defer.inlineCallbacks
def test_put_room_no_keys(self):
# PUT with no config keys, expect new room id
(code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21aa%3Atest", "{}"
)
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@defer.inlineCallbacks
def test_put_room_visibility_key(self):
# PUT with known config keys, expect new room id
(code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21bb%3Atest", '{"visibility":"private"}'
)
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@defer.inlineCallbacks
def test_put_room_custom_key(self):
# PUT with custom config keys, expect new room id
(code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21cc%3Atest", '{"custom":"stuff"}'
)
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@defer.inlineCallbacks
def test_put_room_known_and_unknown_keys(self):
# PUT with custom + known config keys, expect new room id
(code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21dd%3Atest",
'{"visibility":"private","custom":"things"}'
)
self.assertEquals(200, code)
self.assertTrue("room_id" in response)
@defer.inlineCallbacks
def test_put_room_invalid_content(self):
# PUT with invalid content / room names, expect 400
(code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/ee", '{"sdf"'
)
"POST",
"/createRoom",
'{"visibili')
self.assertEquals(400, code)
(code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/ee", '["hello"]'
)
"POST",
"/createRoom",
'["hello"]')
self.assertEquals(400, code)
@defer.inlineCallbacks
def test_put_room_conflict(self):
yield self.create_room_as("!aa:test", self.user_id)
# PUT with conflicting room ID, expect 409
(code, response) = yield self.mock_resource.trigger(
"PUT", "/rooms/%21aa%3Atest", "{}"
)
self.assertEquals(409, code)
class RoomTopicTestCase(RestTestCase):
""" Tests /rooms/$room_id/topic REST events. """
@@ -613,8 +558,6 @@ class RoomTopicTestCase(RestTestCase):
def setUp(self):
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.auth_user_id = self.user_id
self.room_id = "!rid1:test"
self.path = "/rooms/%s/topic" % self.room_id
state_handler = Mock(spec=["handle_new_event"])
state_handler.handle_new_event.return_value = True
@@ -626,12 +569,12 @@ class RoomTopicTestCase(RestTestCase):
"test",
db_pool=None,
http_client=None,
federation=Mock(),
datastore=MemoryDataStore(),
replication_layer=Mock(),
state_handler=state_handler,
persistence_service=persistence_service,
)
hs.get_handlers().federation_handler = Mock()
def _get_user_by_token(token=None):
return hs.parse_userid(self.auth_user_id)
@@ -640,7 +583,8 @@ class RoomTopicTestCase(RestTestCase):
synapse.rest.room.register_servlets(hs, self.mock_resource)
# create the room
yield self.create_room_as(self.room_id, self.user_id)
self.room_id = yield self.create_room_as(self.user_id)
self.path = "/rooms/%s/state/m.room.topic" % self.room_id
def tearDown(self):
pass
@@ -717,7 +661,6 @@ class RoomMemberStateTestCase(RestTestCase):
def setUp(self):
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.auth_user_id = self.user_id
self.room_id = "!rid1:test"
state_handler = Mock(spec=["handle_new_event"])
state_handler.handle_new_event.return_value = True
@@ -729,12 +672,12 @@ class RoomMemberStateTestCase(RestTestCase):
"test",
db_pool=None,
http_client=None,
federation=Mock(),
datastore=MemoryDataStore(),
replication_layer=Mock(),
state_handler=state_handler,
persistence_service=persistence_service,
)
hs.get_handlers().federation_handler = Mock()
def _get_user_by_token(token=None):
return hs.parse_userid(self.auth_user_id)
@@ -742,14 +685,14 @@ class RoomMemberStateTestCase(RestTestCase):
synapse.rest.room.register_servlets(hs, self.mock_resource)
yield self.create_room_as(self.room_id, self.user_id)
self.room_id = yield self.create_room_as(self.user_id)
def tearDown(self):
pass
@defer.inlineCallbacks
def test_invalid_puts(self):
path = "/rooms/%s/members/%s/state" % (self.room_id, self.user_id)
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, '{}')
@@ -783,7 +726,7 @@ class RoomMemberStateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_rooms_members_self(self):
path = "/rooms/%s/members/%s/state" % (
path = "/rooms/%s/state/m.room.member/%s" % (
urllib.quote(self.room_id), self.user_id
)
@@ -804,7 +747,7 @@ class RoomMemberStateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_rooms_members_other(self):
self.other_id = "@zzsid1:red"
path = "/rooms/%s/members/%s/state" % (
path = "/rooms/%s/state/m.room.member/%s" % (
urllib.quote(self.room_id), self.other_id
)
@@ -820,7 +763,7 @@ class RoomMemberStateTestCase(RestTestCase):
@defer.inlineCallbacks
def test_rooms_members_other_custom_keys(self):
self.other_id = "@zzsid1:red"
path = "/rooms/%s/members/%s/state" % (
path = "/rooms/%s/state/m.room.member/%s" % (
urllib.quote(self.room_id), self.other_id
)
@@ -843,7 +786,6 @@ class RoomMessagesTestCase(RestTestCase):
def setUp(self):
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.auth_user_id = self.user_id
self.room_id = "!rid1:test"
state_handler = Mock(spec=["handle_new_event"])
state_handler.handle_new_event.return_value = True
@@ -855,12 +797,12 @@ class RoomMessagesTestCase(RestTestCase):
"test",
db_pool=None,
http_client=None,
federation=Mock(),
datastore=MemoryDataStore(),
replication_layer=Mock(),
state_handler=state_handler,
persistence_service=persistence_service,
)
hs.get_handlers().federation_handler = Mock()
def _get_user_by_token(token=None):
return hs.parse_userid(self.auth_user_id)
@@ -868,16 +810,15 @@ class RoomMessagesTestCase(RestTestCase):
synapse.rest.room.register_servlets(hs, self.mock_resource)
yield self.create_room_as(self.room_id, self.user_id)
self.room_id = yield self.create_room_as(self.user_id)
def tearDown(self):
pass
@defer.inlineCallbacks
def test_invalid_puts(self):
path = "/rooms/%s/messages/%s/mid1" % (
urllib.quote(self.room_id), self.user_id
)
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, '{}')
@@ -905,9 +846,8 @@ class RoomMessagesTestCase(RestTestCase):
@defer.inlineCallbacks
def test_rooms_messages_sent(self):
path = "/rooms/%s/messages/%s/mid1" % (
urllib.quote(self.room_id), self.user_id
)
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)
@@ -923,9 +863,8 @@ class RoomMessagesTestCase(RestTestCase):
# self.assert_dict(json.loads(content), response)
# m.text message type
path = "/rooms/%s/messages/%s/mid2" % (
urllib.quote(self.room_id), self.user_id
)
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))
@@ -933,11 +872,3 @@ class RoomMessagesTestCase(RestTestCase):
# (code, response) = yield self.mock_resource.trigger("GET", path, None)
# self.assertEquals(200, code, msg=str(response))
# self.assert_dict(json.loads(content), response)
# trying to send message in different user path
path = "/rooms/%s/messages/%s/mid2" % (
urllib.quote(self.room_id), "invalid" + self.user_id
)
content = '{"body":"test2","msgtype":"m.text"}'
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(403, code, msg=str(response))

View File

@@ -21,8 +21,10 @@ from twisted.trial 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.
@@ -39,18 +41,19 @@ class RestTestCase(unittest.TestCase):
return self.auth_user_id
@defer.inlineCallbacks
def create_room_as(self, room_id, room_creator, is_public=True, tok=None):
def create_room_as(self, room_creator, is_public=True, tok=None):
temp_id = self.auth_user_id
self.auth_user_id = room_creator
path = "/rooms/%s" % room_id
path = "/createRoom"
content = "{}"
if not is_public:
content = '{"visibility":"private"}'
if tok:
path = path + "?access_token=%s" % tok
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
(code, response) = yield self.mock_resource.trigger("POST", path, content)
self.assertEquals(200, code, msg=str(response))
self.auth_user_id = temp_id
defer.returnValue(response["room_id"])
@defer.inlineCallbacks
def invite(self, room=None, src=None, targ=None, expect_code=200, tok=None):
@@ -71,23 +74,22 @@ class RestTestCase(unittest.TestCase):
expect_code=expect_code)
@defer.inlineCallbacks
def change_membership(self, room=None, src=None, targ=None,
membership=None, expect_code=200, tok=None):
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/members/%s/state" % (room, targ)
path = "/rooms/%s/state/m.room.member/%s" % (room, targ)
if tok:
path = path + "?access_token=%s" % tok
if membership == Membership.LEAVE:
(code, response) = yield self.mock_resource.trigger("DELETE", path,
None)
self.assertEquals(expect_code, code, msg=str(response))
else:
(code, response) = yield self.mock_resource.trigger("PUT", path,
'{"membership":"%s"}' % membership)
self.assertEquals(expect_code, code, msg=str(response))
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
@@ -99,14 +101,14 @@ class RestTestCase(unittest.TestCase):
defer.returnValue(response)
@defer.inlineCallbacks
def send(self, room_id, sender_id, body=None, msg_id=None, tok=None,
def send(self, room_id, body=None, txn_id=None, tok=None,
expect_code=200):
if msg_id is None:
msg_id = "m%s" % (str(time.time()))
if txn_id is None:
txn_id = "m%s" % (str(time.time()))
if body is None:
body = "body_text_here"
path = "/rooms/%s/messages/%s/%s" % (room_id, sender_id, msg_id)
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

View File

@@ -243,21 +243,24 @@ class StateTestCase(unittest.TestCase):
state_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 20)
tup = ("pdu_id", "origin.com", 5)
pdus = [tup]
snapshot = Mock()
snapshot.prev_state_pdu = state_pdu
event_id = "pdu_id@origin.com"
self.persistence.get_latest_pdus_in_context.return_value = pdus
self.persistence.get_current_state_pdu.return_value = state_pdu
def fill_out_prev_events(event):
event.prev_events = [event_id]
event.depth = 6
snapshot.fill_out_prev_events = fill_out_prev_events
yield self.state.handle_new_event(event)
yield self.state.handle_new_event(event, snapshot)
self.assertLess(tup[2], event.depth)
self.assertLess(5, event.depth)
self.assertEquals(1, len(event.prev_events))
prev_id = event.prev_events[0]
self.assertEqual(encode_event_id(tup[0], tup[1]), prev_id)
self.assertEqual(event_id, prev_id)
self.assertEqual(
encode_event_id(state_pdu.pdu_id, state_pdu.origin),

View File

@@ -21,13 +21,23 @@ from synapse.api.events.room import (
RoomMemberEvent, MessageEvent
)
from twisted.internet import defer
from twisted.internet import defer, reactor
from collections import namedtuple
from mock import patch, Mock
import json
import urlparse
from inspect import getcallargs
def get_mock_call_args(pattern_func, mock_func):
""" Return the arguments the mock function was called with interpreted
by the pattern functions argument list.
"""
invoked_args, invoked_kargs = mock_func.call_args
return getcallargs(pattern_func, *invoked_args, **invoked_kargs)
# This is a mock /resource/ not an entire server
class MockHttpResource(HttpServer):
@@ -127,6 +137,15 @@ class MemoryDataStore(object):
self.current_state = {}
self.events = []
class Snapshot(namedtuple("Snapshot", "room_id user_id membership_state")):
def fill_out_prev_events(self, event):
pass
def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
return self.Snapshot(
room_id, user_id, self.get_room_member(user_id, room_id)
)
def register(self, user_id, token, password_hash):
if user_id in self.tokens_to_users.values():
raise StoreError(400, "User in use.")
@@ -183,7 +202,7 @@ class MemoryDataStore(object):
def persist_event(self, event):
if event.type == RoomMemberEvent.TYPE:
room_id = event.room_id
user = event.target_user_id
user = event.state_key
membership = event.membership
self.members.setdefault(room_id, {})[user] = event
@@ -196,7 +215,9 @@ class MemoryDataStore(object):
def get_current_state(self, room_id, event_type=None, state_key=""):
if event_type:
key = (room_id, event_type, state_key)
return self.current_state.get(key)
if self.current_state.get(key):
return [self.current_state.get(key)]
return None
else:
return [
e for e in self.current_state
@@ -214,7 +235,7 @@ class MemoryDataStore(object):
def _format_call(args, kwargs):
return ", ".join(
["%r" % (a) for a in args] +
["%r" % (a) for a in args] +
["%s=%r" % (k, v) for k, v in kwargs.items()]
)
@@ -227,8 +248,11 @@ class DeferredMockCallable(object):
def __init__(self):
self.expectations = []
self.calls = []
def __call__(self, *args, **kwargs):
self.calls.append((args, kwargs))
if not self.expectations:
raise ValueError("%r has no pending calls to handle call(%s)" % (
self, _format_call(args, kwargs))
@@ -239,15 +263,52 @@ class DeferredMockCallable(object):
d.callback(None)
return result
raise AssertionError("Was not expecting call(%s)" %
failure = AssertionError("Was not expecting call(%s)" %
_format_call(args, kwargs)
)
for _, _, d in self.expectations:
try:
d.errback(failure)
except:
pass
raise failure
def expect_call_and_return(self, call, result):
self.expectations.append((call, result, defer.Deferred()))
@defer.inlineCallbacks
def await_calls(self):
while self.expectations:
(_, _, d) = self.expectations.pop(0)
yield d
def await_calls(self, timeout=1000):
deferred = defer.DeferredList(
[d for _, _, d in self.expectations],
fireOnOneErrback=True
)
timer = reactor.callLater(
timeout/1000,
deferred.errback,
AssertionError(
"%d pending calls left: %s"% (
len([e for e in self.expectations if not e[2].called]),
[e for e in self.expectations if not e[2].called]
)
)
)
yield deferred
timer.cancel()
self.calls = []
def assert_had_no_calls(self):
if self.calls:
calls = self.calls
self.calls = []
raise AssertionError("Expected not to received any calls, got:\n" +
"\n".join([
"call(%s)" % _format_call(c[0], c[1]) for c in calls
])
)

View File

@@ -20,9 +20,9 @@ limitations under the License.
'use strict';
angular.module('MatrixWebClientController', ['matrixService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'eventStreamService',
function($scope, $location, $rootScope, matrixService, eventStreamService) {
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService',
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) {
// Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path();
@@ -31,36 +31,29 @@ angular.module('MatrixWebClientController', ['matrixService'])
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
$scope.location = $location.path();
});
// Manage the display of the current config
$scope.config;
// Toggles the config display
$scope.showConfig = function() {
if ($scope.config) {
$scope.config = undefined;
}
else {
$scope.config = matrixService.config();
}
};
$scope.closeConfig = function() {
if ($scope.config) {
$scope.config = undefined;
}
};
if (matrixService.isUserLoggedIn()) {
// eventStreamService.resume();
eventStreamService.resume();
mPresence.start();
}
/**
* Open a given page.
* @param {String} url url of the page
*/
$scope.goToPage = function(url) {
$location.url(url);
};
// Logs the user out
$scope.logout = function() {
// kill the event stream
eventStreamService.stop();
// Do not update presence anymore
mPresence.stop();
// Clean permanent data
matrixService.setConfig({});
matrixService.saveConfig();
@@ -83,7 +76,6 @@ angular.module('MatrixWebClientController', ['matrixService'])
}
};
}]);

View File

@@ -1,3 +1,81 @@
/*** Mobile voodoo ***/
@media all and (max-device-width: 640px) {
#messageTableWrapper {
margin-right: 0px ! important;
}
.leftBlock {
width: 8em ! important;
font-size: 8px ! important;
}
.rightBlock {
width: 0px ! important;
display: none ! important;
}
.avatar {
width: 36px ! important;
}
#header,
#messageTable,
#wrapper,
#roomName,
#controls {
max-width: 640px ! important;
}
#userIdCell,
#usersTableWrapper,
#extraControls {
display: none;
}
#buttonsCell {
width: 60px ! important;
padding-left: 20px ! important;
}
#roomLogo {
display: none;
}
#roomName {
text-align: left ! important;
top: -35px ! important;
}
.bubble {
font-size: 12px ! important;
min-height: 20px ! important;
}
#page {
top: 35px ! important;
bottom: 70px ! important;
}
#header,
#page {
margin: 5px ! important;
}
#header {
padding: 5px ! important;
}
/* stop zoom on select */
select:focus,
textarea,
input
{
font-size: 16px ! important;
}
}
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
@@ -17,7 +95,6 @@ h1 {
left: 0px;
right: 0px;
margin: 20px;
margin: 20px;
}
#wrapper {
@@ -32,8 +109,7 @@ h1 {
text-align: right;
top: -40px;
position: absolute;
font-size: 16pt;
margin-bottom: 10px;
font-size: 16px;
}
#controlPanel {
@@ -50,6 +126,10 @@ h1 {
margin: auto;
}
#buttonsCell {
width: 150px;
}
#inputBarTable {
width: 100%;
}
@@ -111,13 +191,13 @@ h1 {
color: #fff;
margin: 2px;
bottom: 0px;
font-size: 8pt;
font-size: 12px;
word-break: break-all;
}
.userPresence {
text-align: center;
font-size: 8pt;
font-size: 12px;
color: #fff;
background-color: #aaa;
border-bottom: 1px #ddd solid;
@@ -159,7 +239,7 @@ h1 {
background-color: #fff;
color: #888;
font-weight: medium;
font-size: 8pt;
font-size: 12px;
text-align: right;
border-top: 1px #ddd solid;
}
@@ -272,12 +352,70 @@ h1 {
top: 0;
}
/*** Recents ***/
.recentsTable {
max-width: 480px;
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.recentsTable tr {
width: 100%;
}
.recentsTable td {
vertical-align: text-top;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.recentsRoom {
cursor: pointer;
}
.recentsRoom:hover {
background-color: #f8f8ff;
}
.recentsRoomSelected {
background-color: #eee;
}
.recentsRoomName {
font-size: 16px;
padding-top: 7px;
width: auto;
}
.recentsRoomSummaryTS {
color: #888;
font-size: 12px;
width: 7em;
text-align: right;
}
.recentsRoomSummary {
color: #888;
font-size: 12px;
padding-bottom: 5px;
}
/*** Recents in the room page ***/
#roomRecentsTableWrapper {
float: left;
max-width: 320px;
margin-right: 20px;
height: 100%;
overflow-y: auto;
}
/*** Profile ***/
.profile-avatar {
width: 160px;
height: 160px;
display:table-cell;
display: table-cell;
vertical-align: middle;
text-align: center;
}
@@ -293,13 +431,19 @@ h1 {
}
#user-displayname {
font-size: 16pt;
font-size: 24px;
}
/******************************/
#header {
padding-left: 20px;
padding-right: 20px;
#header
{
padding: 20px;
max-width: 1280px;
margin: auto;
}
#logo,
#roomLogo {
max-width: 1280px;
margin: auto;
}
@@ -308,18 +452,6 @@ h1 {
float: right;
}
#config {
position: absolute;
z-index: 100;
top: 100px;
left: 50%;
width: 500px;
margin-left: -250px;
text-align: center;
padding: 20px;
background-color: #aaa;
}
.text_entry_section {
position: fixed;
bottom: 0;

View File

@@ -19,9 +19,13 @@ var matrixWebClient = angular.module('matrixWebClient', [
'MatrixWebClientController',
'LoginController',
'RoomController',
'RoomsController',
'HomeController',
'RecentsController',
'SettingsController',
'UserController',
'matrixService',
'matrixPhoneService',
'MatrixCall',
'eventStreamService',
'eventHandlerService',
'infinite-scroll'
@@ -44,16 +48,20 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
templateUrl: 'room/room.html',
controller: 'RoomController'
}).
when('/rooms', {
templateUrl: 'rooms/rooms.html',
controller: 'RoomsController'
when('/', {
templateUrl: 'home/home.html',
controller: 'HomeController'
}).
when('/settings', {
templateUrl: 'settings/settings.html',
controller: 'SettingsController'
}).
when('/user/:user_matrix_id', {
templateUrl: 'user/user.html',
controller: 'UserController'
}).
otherwise({
redirectTo: '/rooms'
redirectTo: '/'
});
$provide.factory('AccessTokenInterceptor', ['$q', '$rootScope',
@@ -73,13 +81,11 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
$httpProvider.interceptors.push('AccessTokenInterceptor');
}]);
matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', function($location, matrixService, eventStreamService) {
matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) {
// If user auth details are not in cache, go to the login page
if (!matrixService.isUserLoggedIn()) {
eventStreamService.stop();
$location.path("login");
}
else {
// eventStreamService.resume();
}
}]);

View File

@@ -27,13 +27,16 @@ Typically, this service will store events or broadcast them to any listeners
if typically all the $on method would do is update its own $scope.
*/
angular.module('eventHandlerService', [])
.factory('eventHandlerService', ['matrixService', '$rootScope', function(matrixService, $rootScope) {
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
var MSG_EVENT = "MSG_EVENT";
var MEMBER_EVENT = "MEMBER_EVENT";
var PRESENCE_EVENT = "PRESENCE_EVENT";
var CALL_EVENT = "CALL_EVENT";
var InitialSyncDeferred = $q.defer();
$rootScope.events = {
rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
};
$rootScope.presence = {};
@@ -47,17 +50,13 @@ angular.module('eventHandlerService', [])
}
}
var reInitRoom = function(room_id) {
$rootScope.events.rooms[room_id] = {};
$rootScope.events.rooms[room_id].messages = [];
$rootScope.events.rooms[room_id].members = {};
}
var resetRoomMessages = function(room_id) {
if ($rootScope.events.rooms[room_id]) {
$rootScope.events.rooms[room_id].messages = [];
}
};
var handleMessage = function(event, isLiveEvent) {
if ("membership_target" in event.content) {
event.user_id = event.content.membership_target;
}
initRoom(event.room_id);
if (isLiveEvent) {
@@ -96,12 +95,16 @@ angular.module('eventHandlerService', [])
$rootScope.presence[event.content.user_id] = event;
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
};
var handleCallEvent = function(event, isLiveEvent) {
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
};
return {
MSG_EVENT: MSG_EVENT,
MEMBER_EVENT: MEMBER_EVENT,
PRESENCE_EVENT: PRESENCE_EVENT,
CALL_EVENT: CALL_EVENT,
handleEvent: function(event, isLiveEvent) {
@@ -119,6 +122,9 @@ angular.module('eventHandlerService', [])
console.log("Unable to handle event type " + event.type);
break;
}
if (event.type.indexOf('m.call.') == 0) {
handleCallEvent(event, isLiveEvent);
}
},
// isLiveEvents determines whether notifications should be shown, whether
@@ -129,8 +135,18 @@ angular.module('eventHandlerService', [])
}
},
reInitRoom: function(room_id) {
reInitRoom(room_id);
handleInitialSyncDone: function() {
console.log("# handleInitialSyncDone");
InitialSyncDeferred.resolve($rootScope.events, $rootScope.presence);
},
// Returns a promise that resolves when the initialSync request has been processed
waitForInitialSyncCompletion: function() {
return InitialSyncDeferred.promise;
},
resetRoomMessages: function(room_id) {
resetRoomMessages(room_id);
}
};
}]);

View File

@@ -25,7 +25,8 @@ the eventHandlerService.
angular.module('eventStreamService', [])
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
var END = "END";
var TIMEOUT_MS = 30000;
var SERVER_TIMEOUT_MS = 30000;
var CLIENT_TIMEOUT_MS = 40000;
var ERR_TIMEOUT_MS = 5000;
var settings = {
@@ -55,7 +56,7 @@ angular.module('eventStreamService', [])
deferred = deferred || $q.defer();
// run the stream from the latest token
matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
matrixService.getEventStream(settings.from, SERVER_TIMEOUT_MS, CLIENT_TIMEOUT_MS).then(
function(response) {
if (!settings.isActive) {
console.log("[EventStream] Got response but now inactive. Dropping data.");
@@ -80,7 +81,7 @@ angular.module('eventStreamService', [])
}
},
function(error) {
if (error.status == 403) {
if (error.status === 403) {
settings.shouldPoll = false;
}
@@ -96,7 +97,7 @@ angular.module('eventStreamService', [])
);
return deferred.promise;
}
};
var startEventStream = function() {
settings.shouldPoll = true;
@@ -110,18 +111,17 @@ angular.module('eventStreamService', [])
for (var i = 0; i < rooms.length; ++i) {
var room = rooms[i];
if ("state" in room) {
for (var j = 0; j < room.state.length; ++j) {
eventHandlerService.handleEvents(room.state[j], false);
}
eventHandlerService.handleEvents(room.state, false);
}
}
var presence = response.data.presence;
for (var i = 0; i < presence.length; ++i) {
eventHandlerService.handleEvent(presence[i], false);
}
eventHandlerService.handleEvents(presence, false);
settings.from = response.data.end
// Initial sync is done
eventHandlerService.handleInitialSyncDone();
settings.from = response.data.end;
doEventStream(deferred);
},
function(error) {

View File

@@ -0,0 +1,268 @@
/*
Copyright 2014 matrix.org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var forAllVideoTracksOnStream = function(s, f) {
var tracks = s.getVideoTracks();
for (var i = 0; i < tracks.length; i++) {
f(tracks[i]);
}
}
var forAllAudioTracksOnStream = function(s, f) {
var tracks = s.getAudioTracks();
for (var i = 0; i < tracks.length; i++) {
f(tracks[i]);
}
}
var forAllTracksOnStream = function(s, f) {
forAllVideoTracksOnStream(s, f);
forAllAudioTracksOnStream(s, f);
}
angular.module('MatrixCall', [])
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) {
var MatrixCall = function(room_id) {
this.room_id = room_id;
this.call_id = "c" + new Date().getTime();
this.state = 'fledgling';
}
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
MatrixCall.prototype.placeCall = function() {
self = this;
matrixPhoneService.callPlaced(this);
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); });
self.state = 'wait_local_media';
};
MatrixCall.prototype.initWithInvite = function(msg) {
this.msg = msg;
this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]})
self= this;
this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
this.state = 'ringing';
};
MatrixCall.prototype.answer = function() {
console.trace("Answering call "+this.call_id);
self = this;
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); });
this.state = 'wait_local_media';
};
MatrixCall.prototype.hangup = function() {
console.trace("Ending call "+this.call_id);
if (this.localAVStream) {
forAllTracksOnStream(this.localAVStream, function(t) {
t.stop();
});
}
if (this.remoteAVStream) {
forAllTracksOnStream(this.remoteAVStream, function(t) {
t.stop();
});
}
var content = {
version: 0,
call_id: this.call_id,
};
matrixService.sendEvent(this.room_id, 'm.call.hangup', undefined, content).then(this.messageSent, this.messageSendFailed);
this.state = 'ended';
};
MatrixCall.prototype.gotUserMediaForInvite = function(stream) {
this.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]})
self = this;
this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
this.peerConn.addStream(stream);
this.peerConn.createOffer(function(d) {
self.gotLocalOffer(d);
}, function(e) {
self.getLocalOfferFailed(e);
});
this.state = 'create_offer';
};
MatrixCall.prototype.gotUserMediaForAnswer = function(stream) {
this.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
this.peerConn.addStream(stream);
self = this;
var constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': false
},
};
this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints);
this.state = 'create_answer';
};
MatrixCall.prototype.gotLocalIceCandidate = function(event) {
console.trace(event);
if (event.candidate) {
var content = {
version: 0,
call_id: this.call_id,
candidate: event.candidate
};
matrixService.sendEvent(this.room_id, 'm.call.candidate', undefined, content).then(this.messageSent, this.messageSendFailed);
}
}
MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
console.trace("Got ICE candidate from remote: "+cand);
var candidateObject = new RTCIceCandidate({
sdpMLineIndex: cand.label,
candidate: cand.candidate
});
this.peerConn.addIceCandidate(candidateObject, function() {}, function(e) {});
};
MatrixCall.prototype.receivedAnswer = function(msg) {
this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
this.state = 'connecting';
};
MatrixCall.prototype.gotLocalOffer = function(description) {
console.trace("Created offer: "+description);
this.peerConn.setLocalDescription(description);
var content = {
version: 0,
call_id: this.call_id,
offer: description
};
matrixService.sendEvent(this.room_id, 'm.call.invite', undefined, content).then(this.messageSent, this.messageSendFailed);
this.state = 'invite_sent';
};
MatrixCall.prototype.createdAnswer = function(description) {
console.trace("Created answer: "+description);
this.peerConn.setLocalDescription(description);
var content = {
version: 0,
call_id: this.call_id,
answer: description
};
matrixService.sendEvent(this.room_id, 'm.call.answer', undefined, content).then(this.messageSent, this.messageSendFailed);
this.state = 'connecting';
};
MatrixCall.prototype.messageSent = function() {
};
MatrixCall.prototype.messageSendFailed = function(error) {
};
MatrixCall.prototype.getLocalOfferFailed = function(error) {
this.onError("Failed to start audio for call!");
};
MatrixCall.prototype.getUserMediaFailed = function() {
this.onError("Couldn't start capturing audio! Is your microphone set up?");
};
MatrixCall.prototype.onIceConnectionStateChanged = function() {
console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState);
// ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
this.state = 'connected';
}
};
MatrixCall.prototype.onSignallingStateChanged = function() {
console.trace("Signalling state changed to: "+this.peerConn.signalingState);
};
MatrixCall.prototype.onSetRemoteDescriptionSuccess = function() {
console.trace("Set remote description");
};
MatrixCall.prototype.onSetRemoteDescriptionError = function(e) {
console.trace("Failed to set remote description"+e);
};
MatrixCall.prototype.onAddStream = function(event) {
console.trace("Stream added"+event);
var s = event.stream;
this.remoteAVStream = s;
var self = this;
forAllTracksOnStream(s, function(t) {
// not currently implemented in chrome
t.onstarted = self.onRemoteStreamTrackStarted;
});
// not currently implemented in chrome
event.stream.onstarted = this.onRemoteStreamStarted;
var player = new Audio();
player.src = URL.createObjectURL(s);
player.play();
};
MatrixCall.prototype.onRemoteStreamStarted = function(event) {
this.state = 'connected';
};
MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) {
this.state = 'connected';
};
MatrixCall.prototype.onHangupReceived = function() {
this.state = 'ended';
if (this.localAVStream) {
forAllTracksOnStream(this.localAVStream, function(t) {
t.stop();
});
}
if (this.remoteAVStream) {
forAllTracksOnStream(this.remoteAVStream, function(t) {
t.stop();
});
}
this.onHangup();
};
return MatrixCall;
}]);

View File

@@ -0,0 +1,68 @@
/*
Copyright 2014 matrix.org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('matrixPhoneService', [])
.factory('matrixPhoneService', ['$rootScope', '$injector', 'matrixService', 'eventHandlerService', function MatrixPhoneService($rootScope, $injector, matrixService, eventHandlerService) {
var matrixPhoneService = function() {
};
matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
matrixPhoneService.allCalls = {};
matrixPhoneService.callPlaced = function(call) {
matrixPhoneService.allCalls[call.call_id] = call;
};
$rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
if (!isLive) return; // until matrix supports expiring messages
if (event.user_id == matrixService.config().user_id) return;
var msg = event.content;
if (event.type == 'm.call.invite') {
var MatrixCall = $injector.get('MatrixCall');
var call = new MatrixCall(event.room_id);
call.call_id = msg.call_id;
call.initWithInvite(msg);
matrixPhoneService.allCalls[call.call_id] = call;
$rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
} else if (event.type == 'm.call.answer') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) {
console.trace("Got answer for unknown call ID "+msg.call_id);
return;
}
call.receivedAnswer(msg);
} else if (event.type == 'm.call.candidate') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) {
console.trace("Got candidate for unknown call ID "+msg.call_id);
return;
}
call.gotRemoteIceCandidate(msg.candidate);
} else if (event.type == 'm.call.hangup') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) {
console.trace("Got hangup for unknown call ID "+msg.call_id);
return;
}
call.onHangupReceived();
matrixPhoneService.allCalls[msg.call_id] = undefined;
}
});
return matrixPhoneService;
}]);

View File

@@ -41,7 +41,7 @@ angular.module('matrixService', [])
var prefixPath = "/matrix/client/api/v1";
var MAPPING_PREFIX = "alias_for_";
var doRequest = function(method, path, params, data) {
var doRequest = function(method, path, params, data, $httpParams) {
if (!config) {
console.warn("No config exists. Cannot perform request to "+path);
return;
@@ -58,7 +58,7 @@ angular.module('matrixService', [])
path = prefixPath + path;
}
return doBaseRequest(config.homeserver, method, path, params, data, undefined);
return doBaseRequest(config.homeserver, method, path, params, data, undefined, $httpParams);
};
var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
@@ -97,7 +97,7 @@ angular.module('matrixService', [])
// Create a room
create: function(room_id, visibility) {
// The REST path spec
var path = "/rooms";
var path = "/createRoom";
return doRequest("POST", path, undefined, {
visibility: visibility,
@@ -106,28 +106,25 @@ angular.module('matrixService', [])
},
// List all rooms joined or been invited to
rooms: function(from, to, limit) {
rooms: function(limit, feedback) {
// The REST path spec
var path = "/im/sync";
return doRequest("GET", path);
var path = "/initialSync";
var params = {};
if (limit) {
params.limit = limit;
}
if (feedback) {
params.feedback = feedback;
}
return doRequest("GET", path, params);
},
// Joins a room
join: function(room_id) {
// The REST path spec
var path = "/rooms/$room_id/members/$user_id/state";
// Like the cmd client, escape room ids
room_id = encodeURIComponent(room_id);
// Customize it
path = path.replace("$room_id", room_id);
path = path.replace("$user_id", config.user_id);
return doRequest("PUT", path, undefined, {
membership: "join"
});
return this.membershipChange(room_id, undefined, "join");
},
joinAlias: function(room_alias) {
@@ -136,44 +133,38 @@ angular.module('matrixService', [])
path = path.replace("$room_alias", room_alias);
return doRequest("PUT", path, undefined, {});
// TODO: PUT with txn ID
return doRequest("POST", path, undefined, {});
},
// Invite a user to a room
invite: function(room_id, user_id) {
// The REST path spec
var path = "/rooms/$room_id/members/$user_id/state";
// Like the cmd client, escape room ids
room_id = encodeURIComponent(room_id);
// Customize it
path = path.replace("$room_id", room_id);
path = path.replace("$user_id", user_id);
return doRequest("PUT", path, undefined, {
membership: "invite"
});
return this.membershipChange(room_id, user_id, "invite");
},
// Leaves a room
leave: function(room_id) {
return this.membershipChange(room_id, undefined, "leave");
},
membershipChange: function(room_id, user_id, membershipValue) {
// The REST path spec
var path = "/rooms/$room_id/members/$user_id/state";
var path = "/rooms/$room_id/$membership";
path = path.replace("$room_id", encodeURIComponent(room_id));
path = path.replace("$membership", encodeURIComponent(membershipValue));
// Like the cmd client, escape room ids
room_id = encodeURIComponent(room_id);
var data = {};
if (user_id !== undefined) {
data = { user_id: user_id };
}
// Customize it
path = path.replace("$room_id", room_id);
path = path.replace("$user_id", config.user_id);
return doRequest("DELETE", path, undefined, undefined);
// TODO: Use PUT with transaction IDs
return doRequest("POST", path, undefined, data);
},
// Retrieves the room ID corresponding to a room alias
resolveRoomAlias:function(room_alias) {
var path = "/matrix/client/api/v1/ds/room/$room_alias";
var path = "/matrix/client/api/v1/directory/room/$room_alias";
room_alias = encodeURIComponent(room_alias);
path = path.replace("$room_alias", room_alias);
@@ -181,12 +172,12 @@ angular.module('matrixService', [])
return doRequest("GET", path, undefined, {});
},
sendMessage: function(room_id, msg_id, content) {
sendEvent: function(room_id, eventType, txn_id, content) {
// The REST path spec
var path = "/rooms/$room_id/messages/$from/$msg_id";
var path = "/rooms/$room_id/send/"+eventType+"/$txn_id";
if (!msg_id) {
msg_id = "m" + new Date().getTime();
if (!txn_id) {
txn_id = "m" + new Date().getTime();
}
// Like the cmd client, escape room ids
@@ -194,12 +185,15 @@ angular.module('matrixService', [])
// Customize it
path = path.replace("$room_id", room_id);
path = path.replace("$from", config.user_id);
path = path.replace("$msg_id", msg_id);
path = path.replace("$txn_id", txn_id);
return doRequest("PUT", path, undefined, content);
},
sendMessage: function(room_id, txn_id, content) {
return this.sendEvent(room_id, 'm.room.message', txn_id, content);
},
// Send a text message
sendTextMessage: function(room_id, body, msg_id) {
var content = {
@@ -236,13 +230,13 @@ angular.module('matrixService', [])
// Like the cmd client, escape room ids
room_id = encodeURIComponent(room_id);
var path = "/rooms/$room_id/members/list";
var path = "/rooms/$room_id/members";
path = path.replace("$room_id", room_id);
return doRequest("GET", path);
},
paginateBackMessages: function(room_id, from_token, limit) {
var path = "/rooms/$room_id/messages/list";
var path = "/rooms/$room_id/messages";
path = path.replace("$room_id", room_id);
var params = {
from: from_token,
@@ -254,7 +248,7 @@ angular.module('matrixService', [])
// get a list of public rooms on your home server
publicRooms: function() {
var path = "/public/rooms"
var path = "/publicRooms"
return doRequest("GET", path);
},
@@ -353,15 +347,31 @@ angular.module('matrixService', [])
return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
},
// start listening on /events
getEventStream: function(from, timeout) {
/**
* Start listening on /events
* @param {String} from the token from which to listen events to
* @param {Integer} serverTimeout the time in ms the server will hold open the connection
* @param {Integer} clientTimeout the timeout in ms used at the client HTTP request level
* @returns a promise
*/
getEventStream: function(from, serverTimeout, clientTimeout) {
var path = "/events";
var params = {
from: from,
timeout: timeout
timeout: serverTimeout
};
return doRequest("GET", path, params);
var $httpParams;
if (clientTimeout) {
// If the Internet connection is lost, this timeout is used to be able to
// cancel the current request and notify the client so that it can retry with a new request.
$httpParams = {
timeout: clientTimeout
};
}
return doRequest("GET", path, params, undefined, $httpParams);
},
// Indicates if user authentications details are stored in cache
@@ -377,6 +387,23 @@ angular.module('matrixService', [])
return false;
}
},
// Enum of presence state
presence: {
offline: "offline",
unavailable: "unavailable",
online: "online",
free_for_chat: "free_for_chat"
},
// Set the logged in user presence state
setUserPresence: function(presence) {
var path = "/presence/$user_id/status";
path = path.replace("$user_id", config.user_id);
return doRequest("PUT", path, undefined, {
state: presence
});
},
/****** Permanent storage of user information ******/
@@ -408,6 +435,44 @@ angular.module('matrixService', [])
config.version = configVersion;
localStorage.setItem("config", JSON.stringify(config));
},
/****** Room aliases management ******/
/**
* Get the room_alias & room_display_name which are computed from data
* already retrieved from the server.
* @param {Room object} room one element of the array returned by the response
* of rooms() and publicRooms()
* @returns {Object} {room_alias: "...", room_display_name: "..."}
*/
getRoomAliasAndDisplayName: function(room) {
var result = {
room_alias: undefined,
room_display_name: undefined
};
var alias = this.getRoomIdToAliasMapping(room.room_id);
if (alias) {
// use the existing alias from storage
result.room_alias = alias;
result.room_display_name = alias;
}
else if (room.aliases && room.aliases[0]) {
// save the mapping
// TODO: select the smarter alias from the array
this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]);
result.room_display_name = room.aliases[0];
}
else if (room.membership === "invite" && "inviter" in room) {
result.room_display_name = room.inviter + "'s room";
}
else {
// last resort use the room id
result.room_display_name = room.room_id;
}
return result;
},
createRoomIdToAliasMapping: function(roomId, alias) {
localStorage.setItem(MAPPING_PREFIX+roomId, alias);

View File

@@ -0,0 +1,113 @@
/*
Copyright 2014 matrix.org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
/*
* This service tracks user activity on the page to determine his presence state.
* Any state change will be sent to the Home Server.
*/
angular.module('mPresence', [])
.service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) {
// Time in ms after that a user is considered as unavailable/away
var UNAVAILABLE_TIME = 5 * 60000; // 5 mins
// The current presence state
var state = undefined;
var self =this;
var timer;
/**
* Start listening the user activity to evaluate his presence state.
* Any state change will be sent to the Home Server.
*/
this.start = function() {
if (undefined === state) {
// The user is online if he moves the mouser or press a key
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
resetTimer();
}
};
/**
* Stop tracking user activity
*/
this.stop = function() {
if (timer) {
$timeout.cancel(timer);
timer = undefined;
}
state = undefined;
};
/**
* Get the current presence state.
* @returns {matrixService.presence} the presence state
*/
this.getState = function() {
return state;
};
/**
* Set the presence state.
* If the state has changed, the Home Server will be notified.
* @param {matrixService.presence} newState the new presence state
*/
this.setState = function(newState) {
if (newState !== state) {
console.log("mPresence - New state: " + newState);
state = newState;
// Inform the HS on the new user state
matrixService.setUserPresence(state).then(
function() {
},
function(error) {
console.log("mPresence - Failed to send new presence state: " + JSON.stringify(error));
});
}
};
/**
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
* @private
*/
function onUnvailableTimerFire() {
self.setState(matrixService.presence.unavailable);
}
/**
* Callback called when the user made an action on the page
* @private
*/
function resetTimer() {
// User is still here
self.setState(matrixService.presence.online);
// Re-arm the timer
$timeout.cancel(timer);
timer = $timeout(onUnvailableTimerFire, UNAVAILABLE_TIME);
}
}]);

View File

@@ -0,0 +1,113 @@
/*
Copyright 2014 matrix.org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
.controller('HomeController', ['$scope', '$location', 'matrixService',
function($scope, $location, matrixService) {
$scope.config = matrixService.config();
$scope.public_rooms = [];
$scope.newRoomId = "";
$scope.feedback = "";
$scope.newRoom = {
room_id: "",
private: false
};
$scope.goToRoom = {
room_id: ""
};
$scope.joinAlias = {
room_alias: ""
};
var refresh = function() {
matrixService.publicRooms().then(
function(response) {
$scope.public_rooms = response.data.chunk;
for (var i = 0; i < $scope.public_rooms.length; i++) {
var room = $scope.public_rooms[i];
// Add room_alias & room_display_name members
angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
}
}
);
};
$scope.createNewRoom = function(room_id, isPrivate) {
var visibility = "public";
if (isPrivate) {
visibility = "private";
}
matrixService.create(room_id, visibility).then(
function(response) {
// This room has been created. Refresh the rooms list
console.log("Created room " + response.data.room_alias + " with id: "+
response.data.room_id);
matrixService.createRoomIdToAliasMapping(
response.data.room_id, response.data.room_alias);
refresh();
},
function(error) {
$scope.feedback = "Failure: " + error.data;
});
};
// Go to a room
$scope.goToRoom = function(room_id) {
// Simply open the room page on this room id
//$location.url("room/" + room_id);
matrixService.join(room_id).then(
function(response) {
if (response.data.hasOwnProperty("room_id")) {
if (response.data.room_id != room_id) {
$location.url("room/" + response.data.room_id);
return;
}
}
$location.url("room/" + room_id);
},
function(error) {
$scope.feedback = "Can't join room: " + error.data;
}
);
};
$scope.joinAlias = function(room_alias) {
matrixService.joinAlias(room_alias).then(
function(response) {
// Go to this room
$location.url("room/" + room_alias);
},
function(error) {
$scope.feedback = "Can't join room: " + error.data;
}
);
};
$scope.onInit = function() {
refresh();
};
}]);

58
webclient/home/home.html Normal file
View File

@@ -0,0 +1,58 @@
<div ng-controller="HomeController" data-ng-init="onInit()">
<div id="page">
<div id="wrapper">
<div>
<form>
<table>
<tr>
<td>
<div class="profile-avatar">
<img ng-src="{{ config.avatarUrl || 'img/default-profile.jpg' }}"/>
</div>
</td>
<td>
<div id="user-ids">
<div id="user-displayname">{{ config.displayName }}</div>
<div>{{ config.user_id }}</div>
</div>
</td>
</tr>
</table>
</form>
</div>
<h3>Recents</h3>
<div ng-include="'recents/recents.html'"></div>
<br/>
<h3>Public rooms</h3>
<div class="public_rooms" ng-repeat="room in public_rooms">
<div>
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
</div>
</div>
<br/>
<div>
<form>
<input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
<input type="checkbox" ng-model="newRoom.private">private
<button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>
</form>
</div>
<div>
<form>
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
</form>
</div>
<br/>
{{ feedback }}
</div>
</div>
</div>

View File

@@ -2,10 +2,12 @@
<html xmlns:ng="http://angularjs.org" ng-app="matrixWebClient" ng-controller="MatrixWebClientController">
<head>
<title>[matrix]</title>
<link rel="stylesheet" href="app.css">
<link rel="icon" href="favicon.ico">
<meta name="viewport" content="width=device-width">
<script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
<script src="js/angular.min.js"></script>
<script src="js/angular-route.min.js"></script>
@@ -15,14 +17,20 @@
<script src="app-controller.js"></script>
<script src="app-directive.js"></script>
<script src="app-filter.js"></script>
<script src="home/home-controller.js"></script>
<script src="login/login-controller.js"></script>
<script src="recents/recents-controller.js"></script>
<script src="recents/recents-filter.js"></script>
<script src="room/room-controller.js"></script>
<script src="room/room-directive.js"></script>
<script src="rooms/rooms-controller.js"></script>
<script src="settings/settings-controller.js"></script>
<script src="user/user-controller.js"></script>
<script src="components/matrix/matrix-service.js"></script>
<script src="components/matrix/matrix-call.js"></script>
<script src="components/matrix/matrix-phone-service.js"></script>
<script src="components/matrix/event-stream-service.js"></script>
<script src="components/matrix/event-handler-service.js"></script>
<script src="components/matrix/presence-service.js"></script>
<script src="components/fileInput/file-input-directive.js"></script>
<script src="components/fileUpload/file-upload-service.js"></script>
<script src="components/utilities/utilities-service.js"></script>
@@ -33,22 +41,11 @@
<header id="header">
<!-- Do not show buttons on the login page -->
<div id="header-buttons" ng-hide="'/login' == location ">
<button ng-click="showConfig()">Config</button>
<button ng-click='goToPage("settings")'>Settings</button>
<button ng-click="logout()">Log out</button>
</div>
<h1>[matrix]</h1>
</header>
<div id="config" ng-hide="!config">
<div>Home server: {{ config.homeserver }} </div>
<div>User ID: {{ config.user_id }} </div>
<div>Access token: {{ config.access_token }} </div>
<div><button ng-click="requestNotifications()">Request notifications</button></div>
<div><button ng-click="closeConfig()">Close</button></div>
</div>
<div ng-view></div>
</body>

View File

@@ -53,7 +53,7 @@ angular.module('LoginController', ['matrixService'])
matrixService.saveConfig();
eventStreamService.resume();
// Go to the user's rooms list page
$location.url("rooms");
$location.url("home");
},
function(error) {
if (error.data) {
@@ -86,7 +86,7 @@ angular.module('LoginController', ['matrixService'])
});
matrixService.saveConfig();
eventStreamService.resume();
$location.url("rooms");
$location.url("home");
}
else {
$scope.feedback = "Failed to login: " + JSON.stringify(response.data);

View File

@@ -1,4 +1,6 @@
<div ng-controller="LoginController" class="login">
<div ng-controller="LoginController" class="login">
<h1 id="logo">[matrix]</h1>
<div id="page">
<div id="wrapper">

View File

@@ -0,0 +1,90 @@
/*
Copyright 2014 matrix.org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService',
function($scope, matrixService, eventHandlerService) {
$scope.rooms = {};
// $scope of the parent where the recents component is included can override this value
// in order to highlight a specific room in the list
$scope.recentsSelectedRoomID;
var listenToEventStream = function() {
// Refresh the list on matrix invitation and message event
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
var config = matrixService.config();
if (isLive && event.state_key === config.user_id && event.content.membership === "invite") {
console.log("Invited to room " + event.room_id);
// FIXME push membership to top level key to match /im/sync
event.membership = event.content.membership;
// FIXME bodge a nicer name than the room ID for this invite.
event.room_display_name = event.user_id + "'s room";
$scope.rooms[event.room_id] = event;
}
});
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
if (isLive) {
$scope.rooms[event.room_id].lastMsg = event;
}
});
};
var refresh = function() {
// List all rooms joined or been invited to
// TODO: This is a pity that event-stream-service.js makes the same call
// We should be able to reuse event-stream-service.js fetched data
matrixService.rooms(1, false).then(
function(response) {
// Reset data
$scope.rooms = {};
var rooms = response.data.rooms;
for (var i=0; i<rooms.length; i++) {
var room = rooms[i];
// Add room_alias & room_display_name members
$scope.rooms[room.room_id] = angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
// Create a shortcut for the last message of this room
if (room.messages && room.messages.chunk && room.messages.chunk[0]) {
$scope.rooms[room.room_id].lastMsg = room.messages.chunk[0];
}
}
var presence = response.data.presence;
for (var i = 0; i < presence.length; ++i) {
eventHandlerService.handleEvent(presence[i], false);
}
// From now, update recents from the stream
listenToEventStream();
},
function(error) {
$scope.feedback = "Failure: " + error.data;
}
);
};
$scope.onInit = function() {
refresh();
};
}]);

View File

@@ -0,0 +1,47 @@
/*
Copyright 2014 matrix.org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('RecentsController')
.filter('orderRecents', function() {
return function(rooms) {
// Transform the dict into an array
// The key, room_id, is already in value objects
var filtered = [];
angular.forEach(rooms, function(value, key) {
filtered.push( value );
});
// And time sort them
// The room with the lastest message at first
filtered.sort(function (a, b) {
// Invite message does not have a body message nor ts
// Puth them at the top of the list
if (undefined === a.lastMsg) {
return -1;
}
else if (undefined === b.lastMsg) {
return 1;
}
else {
return b.lastMsg.ts - a.lastMsg.ts;
}
});
return filtered;
};
});

View File

@@ -0,0 +1,61 @@
<div ng-controller="RecentsController" data-ng-init="onInit()">
<table class="recentsTable">
<tbody ng-repeat="(rm_id, room) in rooms | orderRecents"
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
class ="recentsRoom"
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
<tr>
<td class="recentsRoomName">
{{ room.room_display_name }}
</td>
<td class="recentsRoomSummaryTS">
{{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
</td>
</tr>
<tr>
<td colspan="2" class="recentsRoomSummary">
<div ng-show="room.membership === 'invite'" >
{{ room.inviter }} invited you
</div>
<div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" >
<div ng-switch-when="m.room.member">
{{ room.lastMsg.user_id }}
{{ {"join": "joined", "leave": "left", "invite": "invited"}[room.lastMsg.content.membership] }}
{{ room.lastMsg.content.membership === "invite" ? (room.lastMsg.state_key || '') : '' }}
</div>
<div ng-switch-when="m.room.message">
<div ng-switch="room.lastMsg.content.msgtype">
<div ng-switch-when="m.text">
{{ room.lastMsg.user_id }} :
<span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'">
</span>
</div>
<div ng-switch-when="m.image">
{{ room.lastMsg.user_id }} sent an image
</div>
<div ng-switch-when="m.emote">
<span ng-bind-html="'* ' + (room.lastMsg.user_id) + ' ' + room.lastMsg.content.body | linky:'_blank'">
</span>
</div>
<div ng-switch-default>
{{ room.lastMsg.content }}
</div>
</div>
</div>
<div ng-switch-default>
{{ room.lastMsg }}
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
angular.module('RoomController', ['ngSanitize', 'mUtilities'])
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
angular.module('RoomController', ['ngSanitize', 'mFileInput'])
.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall',
function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) {
'use strict';
var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320;
@@ -51,21 +51,20 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
objDiv.scrollTop = objDiv.scrollHeight;
}, 0);
};
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
if (isLive && event.room_id === $scope.room_id) {
scrollToBottom();
if (window.Notification) {
// FIXME: we should also notify based on a timer or other heuristics
// rather than the window being minimised
if (document.hidden) {
// Show notification when the user is idle
if (matrixService.presence.offline === mPresence.getState()) {
var notification = new window.Notification(
($scope.members[event.user_id].displayname || event.user_id) +
" (" + ($scope.room_alias || $scope.room_id) + ")", // FIXME: don't leak room_ids here
{
"body": event.content.body,
"icon": $scope.members[event.user_id].avatar_url,
"icon": $scope.members[event.user_id].avatar_url
});
$timeout(function() {
notification.close();
@@ -82,6 +81,17 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
$scope.$on(eventHandlerService.PRESENCE_EVENT, function(ngEvent, event, isLive) {
updatePresence(event);
});
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
console.trace("incoming call");
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
$scope.currentCall = call;
});
$scope.memberCount = function() {
return Object.keys($scope.members).length;
};
$scope.paginateMore = function() {
if ($scope.state.can_paginate) {
@@ -89,6 +99,15 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
paginate(MESSAGES_PER_PAGINATION);
}
};
$scope.answerCall = function() {
$scope.currentCall.answer();
};
$scope.hangupCall = function() {
$scope.currentCall.hangup();
$scope.currentCall = undefined;
};
var paginate = function(numItems) {
// console.log("paginate " + numItems);
@@ -154,7 +173,10 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
var updateMemberList = function(chunk) {
if (chunk.room_id != $scope.room_id) return;
var isNewMember = !(chunk.target_user_id in $scope.members);
// set target_user_id to keep things clear
var target_user_id = chunk.state_key;
var isNewMember = !(target_user_id in $scope.members);
if (isNewMember) {
// FIXME: why are we copying these fields around inside chunk?
if ("state" in chunk.content) {
@@ -172,7 +194,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
if ("avatar_url" in chunk.content) {
chunk.avatar_url = chunk.content.avatar_url;
}
$scope.members[chunk.target_user_id] = chunk;
$scope.members[target_user_id] = chunk;
/*
// Stale code for explicitly hammering the homeserver for every displayname & avatar_url
@@ -202,16 +224,16 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
});
*/
if (chunk.target_user_id in $rootScope.presence) {
updatePresence($rootScope.presence[chunk.target_user_id]);
if (target_user_id in $rootScope.presence) {
updatePresence($rootScope.presence[target_user_id]);
}
}
else {
// selectively update membership else it will nuke the picture and displayname too :/
var member = $scope.members[chunk.target_user_id];
var member = $scope.members[target_user_id];
member.content.membership = chunk.content.membership;
}
}
};
var updatePresence = function(chunk) {
if (!(chunk.content.user_id in $scope.members)) {
@@ -238,10 +260,10 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
if ("avatar_url" in chunk.content) {
member.avatar_url = chunk.content.avatar_url;
}
}
};
$scope.send = function() {
if ($scope.textInput == "") {
if ($scope.textInput === "") {
return;
}
@@ -250,7 +272,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
// Send the text message
var promise;
// FIXME: handle other commands too
if ($scope.textInput.indexOf("/me") == 0) {
if ($scope.textInput.indexOf("/me") === 0) {
promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4));
}
else {
@@ -279,7 +301,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
}
if (room_id_or_alias && '!' === room_id_or_alias[0]) {
// Yes. We can start right now
// Yes. We can go on right now
$scope.room_id = room_id_or_alias;
$scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
onInit2();
@@ -310,7 +332,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
$scope.room_id = response.data.room_id;
console.log(" -> Room ID: " + $scope.room_id);
// Now, we can start
// Now, we can go on
onInit2();
},
function () {
@@ -320,34 +342,61 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
});
}
};
var onInit2 = function() {
eventHandlerService.reInitRoom($scope.room_id);
// Join the room
matrixService.join($scope.room_id).then(
console.log("onInit2");
// Make sure the initialSync has been before going further
eventHandlerService.waitForInitialSyncCompletion().then(
function() {
console.log("Joined room "+$scope.room_id);
// Get the current member list
matrixService.getMemberList($scope.room_id).then(
function(response) {
for (var i = 0; i < response.data.chunk.length; i++) {
var chunk = response.data.chunk[i];
updateMemberList(chunk);
}
eventStreamService.resume();
},
function(error) {
$scope.feedback = "Failed get member list: " + error.data.error;
}
);
var needsToJoin = true;
paginate(MESSAGES_PER_PAGINATION);
},
function(reason) {
$scope.feedback = "Can't join room: " + reason;
});
// The room members is available in the data fetched by initialSync
if ($rootScope.events.rooms[$scope.room_id]) {
var members = $rootScope.events.rooms[$scope.room_id].members;
// Update the member list
for (var i in members) {
var member = members[i];
updateMemberList(member);
}
// Check if the user has already join the room
if ($scope.state.user_id in members) {
if ("join" === members[$scope.state.user_id].membership) {
needsToJoin = false;
}
}
}
// Do we to join the room before starting?
if (needsToJoin) {
matrixService.join($scope.room_id).then(
function() {
console.log("Joined room "+$scope.room_id);
onInit3();
},
function(reason) {
$scope.feedback = "Can't join room: " + reason;
});
}
else {
onInit3();
}
}
);
};
var onInit3 = function() {
console.log("onInit3");
// TODO: We should be able to keep them
eventHandlerService.resetRoomMessages($scope.room_id);
// Make recents highlight the current room
$scope.recentsSelectedRoomID = $scope.room_id;
paginate(MESSAGES_PER_PAGINATION);
};
$scope.inviteUser = function(user_id) {
@@ -372,7 +421,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
matrixService.leave($scope.room_id).then(
function(response) {
console.log("Left room ");
$location.url("rooms");
$location.url("home");
},
function(error) {
$scope.feedback = "Failed to leave room: " + error.data.error;
@@ -424,4 +473,21 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
$scope.loadMoreHistory = function() {
paginate(MESSAGES_PER_PAGINATION);
};
$scope.startVoiceCall = function() {
var call = new MatrixCall($scope.room_id);
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
call.placeCall();
$scope.currentCall = call;
}
$scope.onCallError = function(errStr) {
$scope.feedback = errStr;
}
$scope.onCallHangup = function() {
$scope.feedback = "Call ended";
$scope.currentCall = undefined;
}
}]);

View File

@@ -1,4 +1,5 @@
<div ng-controller="RoomController" data-ng-init="onInit()" class="room">
<h1 id="roomLogo">[matrix]</h1>
<div id="page">
<div id="wrapper">
@@ -6,7 +7,11 @@
<div id="roomName">
{{ room_alias || room_id }}
</div>
<div id="roomRecentsTableWrapper">
<div ng-include="'recents/recents.html'"></div>
</div>
<div id="usersTableWrapper">
<table id="usersTable">
<tr ng-repeat="member in members | orderMembersList">
@@ -32,7 +37,7 @@
ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
<td class="leftBlock">
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
<div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm:ss' }}</div>
<div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div>
</td>
<td class="avatar">
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
@@ -40,13 +45,13 @@
</td>
<td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
<div class="bubble">
<span ng-hide='msg.type !== "m.room.member"'>
<span ng-show='msg.type === "m.room.member"'>
{{ members[msg.user_id].displayname || msg.user_id }}
{{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
{{ msg.content.target_id || '' }}
{{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }}
</span>
<span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
<span ng-hide='msg.content.msgtype !== "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
<span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
<span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
<div ng-show='msg.content.msgtype === "m.image"'>
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
<img class="image" ng-src="{{ msg.content.url }}"/>
@@ -73,31 +78,38 @@
<div id="controls">
<table id="inputBarTable">
<tr>
<td width="1">
<td id="userIdCell" width="1px">
{{ state.user_id }}
</td>
<td width="*" style="min-width: 100px">
<td width="*">
<input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
</td>
<td width="150px">
<td id="buttonsCell">
<button ng-click="send()">Send</button>
<button m-file-input="imageFileToSend">Send Image</button>
</td>
<td width="1">
<button m-file-input="imageFileToSend">Image</button>
</td>
</tr>
</table>
<span>
Invite a user:
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
</span>
<button ng-click="leaveRoom()">Leave</button>
<button ng-click="loadMoreHistory()" ng-disabled="!state.can_paginate">Load more history</button>
<div id="extraControls">
<span>
Invite a user:
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
</span>
<button ng-click="leaveRoom()">Leave</button>
<button ng-click="startVoiceCall()" ng-show="currentCall == undefined && memberCount() == 2">Voice Call</button>
<div ng-show="currentCall.state == 'ringing'">
Incoming call from {{ currentCall.user_id }}
<button ng-click="answerCall()">Answer</button>
<button ng-click="hangupCall()">Reject</button>
</div>
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing'">Hang up</button>
<span style="display: none; ">{{ currentCall.state }}</span>
</div>
{{ feedback }}
<div ng-hide="!state.stream_failure">
<div ng-show="state.stream_failure">
{{ state.stream_failure.data.error || "Connection failure" }}
</div>
</div>

View File

@@ -1,300 +0,0 @@
/*
Copyright 2014 matrix.org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService', 'eventStreamService',
function($scope, $location, matrixService, mFileUpload, eventHandlerService, eventStreamService) {
$scope.rooms = {};
$scope.public_rooms = [];
$scope.newRoomId = "";
$scope.feedback = "";
$scope.newRoom = {
room_id: "",
private: false
};
$scope.goToRoom = {
room_id: "",
};
$scope.joinAlias = {
room_alias: "",
};
$scope.newProfileInfo = {
name: matrixService.config().displayName,
avatar: matrixService.config().avatarUrl,
avatarFile: undefined
};
$scope.linkedEmails = {
linkNewEmail: "", // the email entry box
emailBeingAuthed: undefined, // to populate verification text
authTokenId: undefined, // the token id from the IS
clientSecret: undefined, // our client secret
sendAttempt: 1,
emailCode: "", // the code entry box
linkedEmailList: matrixService.config().emailList // linked email list
};
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
var config = matrixService.config();
if (event.target_user_id === config.user_id && event.content.membership === "invite") {
console.log("Invited to room " + event.room_id);
// FIXME push membership to top level key to match /im/sync
event.membership = event.content.membership;
// FIXME bodge a nicer name than the room ID for this invite.
event.room_display_name = event.user_id + "'s room";
$scope.rooms[event.room_id] = event;
}
});
var assignRoomAliases = function(data) {
for (var i=0; i<data.length; i++) {
var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id);
if (alias) {
// use the existing alias from storage
data[i].room_alias = alias;
data[i].room_display_name = alias;
}
else if (data[i].aliases && data[i].aliases[0]) {
// save the mapping
// TODO: select the smarter alias from the array
matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
data[i].room_display_name = data[i].aliases[0];
}
else if (data[i].membership == "invite" && "inviter" in data[i]) {
data[i].room_display_name = data[i].inviter + "'s room"
}
else {
// last resort use the room id
data[i].room_display_name = data[i].room_id;
}
}
return data;
};
$scope.refresh = function() {
// List all rooms joined or been invited to
matrixService.rooms().then(
function(response) {
var data = assignRoomAliases(response.data.rooms);
$scope.feedback = "Success";
for (var i=0; i<data.length; i++) {
$scope.rooms[data[i].room_id] = data[i];
}
var presence = response.data.presence;
for (var i = 0; i < presence.length; ++i) {
eventHandlerService.handleEvent(presence[i], false);
}
},
function(error) {
$scope.feedback = "Failure: " + error.data;
});
matrixService.publicRooms().then(
function(response) {
$scope.public_rooms = assignRoomAliases(response.data.chunk);
}
);
eventStreamService.resume();
};
$scope.createNewRoom = function(room_id, isPrivate) {
var visibility = "public";
if (isPrivate) {
visibility = "private";
}
matrixService.create(room_id, visibility).then(
function(response) {
// This room has been created. Refresh the rooms list
console.log("Created room " + response.data.room_alias + " with id: "+
response.data.room_id);
matrixService.createRoomIdToAliasMapping(
response.data.room_id, response.data.room_alias);
$scope.refresh();
},
function(error) {
$scope.feedback = "Failure: " + error.data;
});
};
// Go to a room
$scope.goToRoom = function(room_id) {
// Simply open the room page on this room id
//$location.url("room/" + room_id);
matrixService.join(room_id).then(
function(response) {
if (response.data.hasOwnProperty("room_id")) {
if (response.data.room_id != room_id) {
$location.url("room/" + response.data.room_id);
return;
}
}
$location.url("room/" + room_id);
},
function(error) {
$scope.feedback = "Can't join room: " + error.data;
}
);
};
$scope.joinAlias = function(room_alias) {
matrixService.joinAlias(room_alias).then(
function(response) {
// Go to this room
$location.url("room/" + room_alias);
},
function(error) {
$scope.feedback = "Can't join room: " + error.data;
}
);
};
$scope.setDisplayName = function(newName) {
matrixService.setDisplayName(newName).then(
function(response) {
$scope.feedback = "Updated display name.";
var config = matrixService.config();
config.displayName = newName;
matrixService.setConfig(config);
matrixService.saveConfig();
},
function(error) {
$scope.feedback = "Can't update display name: " + error.data;
}
);
};
$scope.$watch("newProfileInfo.avatarFile", function(newValue, oldValue) {
if ($scope.newProfileInfo.avatarFile) {
console.log("Uploading new avatar file...");
mFileUpload.uploadFile($scope.newProfileInfo.avatarFile).then(
function(url) {
$scope.newProfileInfo.avatar = url;
$scope.setAvatar($scope.newProfileInfo.avatar);
},
function(error) {
$scope.feedback = "Can't upload image";
}
);
}
});
$scope.setAvatar = function(newUrl) {
console.log("Updating avatar to "+newUrl);
matrixService.setProfilePictureUrl(newUrl).then(
function(response) {
console.log("Updated avatar");
$scope.feedback = "Updated avatar.";
var config = matrixService.config();
config.avatarUrl = newUrl;
matrixService.setConfig(config);
matrixService.saveConfig();
},
function(error) {
$scope.feedback = "Can't update avatar: " + error.data;
}
);
};
var generateClientSecret = function() {
var ret = "";
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 32; i++) {
ret += chars.charAt(Math.floor(Math.random() * chars.length));
}
return ret;
};
$scope.linkEmail = function(email) {
if (email != $scope.linkedEmails.emailBeingAuthed) {
$scope.linkedEmails.clientSecret = generateClientSecret();
$scope.linkedEmails.sendAttempt = 1;
}
matrixService.linkEmail(email, $scope.linkedEmails.clientSecret, $scope.linkedEmails.sendAttempt).then(
function(response) {
if (response.data.success === true) {
$scope.linkedEmails.authTokenId = response.data.sid;
$scope.emailFeedback = "You have been sent an email.";
$scope.linkedEmails.emailBeingAuthed = email;
}
else {
$scope.emailFeedback = "Failed to send email.";
}
},
function(error) {
$scope.emailFeedback = "Can't send email: " + error.data;
}
);
};
$scope.submitEmailCode = function(code) {
var tokenId = $scope.linkedEmails.authTokenId;
if (tokenId === undefined) {
$scope.emailFeedback = "You have not requested a code with this email.";
return;
}
matrixService.authEmail(matrixService.config().user_id, tokenId, code, $scope.linkedEmails.clientSecret).then(
function(response) {
if ("success" in response.data && response.data.success === false) {
$scope.emailFeedback = "Failed to authenticate email.";
return;
}
matrixService.bindEmail(matrixService.config().user_id, tokenId, $scope.linkedEmails.clientSecret).then(
function(response) {
var config = matrixService.config();
var emailList = {};
if ("emailList" in config) {
emailList = config.emailList;
}
emailList[$scope.linkedEmails.emailBeingAuthed] = response;
// save the new email list
config.emailList = emailList;
matrixService.setConfig(config);
matrixService.saveConfig();
// invalidate the email being authed and update UI.
$scope.linkedEmails.emailBeingAuthed = undefined;
$scope.emailFeedback = "";
$scope.linkedEmails.linkedEmailList = emailList;
$scope.linkedEmails.linkNewEmail = "";
$scope.linkedEmails.emailCode = "";
}, function(reason) {
$scope.emailFeedback = "Failed to link email: " + reason;
}
);
},
function(reason) {
$scope.emailFeedback = "Failed to auth email: " + reason;
}
);
};
$scope.refresh();
}]);

View File

@@ -1,101 +0,0 @@
<div ng-controller="RoomsController" class="rooms">
<div id="page">
<div id="wrapper">
<div>
<form>
<table>
<tr>
<td>
<div class="profile-avatar">
<img ng-src="{{ newProfileInfo.avatar || 'img/default-profile.jpg' }}" m-file-input="newProfileInfo.avatarFile"/>
</div>
</td>
<td>
<!-- TODO: To enable once we have an upload server
<button m-file-input="newProfileInfo.avatarFile">Upload new Avatar</button>
or use an existing image URL:
-->
<div>
<input size="40" ng-model="newProfileInfo.avatar" ng-enter="setAvatar(newProfileInfo.avatar)" placeholder="Image URL"/>
<button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar URL</button>
</div>
</td>
</tr>
</table>
</form>
</div>
<div>
<form>
<input size="40" ng-model="newProfileInfo.name" ng-enter="setDisplayName(newProfileInfo.name)" />
<button ng-disabled="!newProfileInfo.name" ng-click="setDisplayName(newProfileInfo.name)">Update Name</button>
</form>
</div>
<br/>
<div>
<form>
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
Link Email
</button>
{{ emailFeedback }}
</form>
<form ng-hide="!linkedEmails.emailBeingAuthed">
Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
<br />
<input size="20" ng-model="linkedEmails.emailCode" ng-enter="submitEmailCode(linkedEmails.emailCode)" />
<button ng-disabled="!linkedEmails.emailCode || !linkedEmails.linkNewEmail" ng-click="submitEmailCode(linkedEmails.emailCode)">
Submit Code
</button>
</form>
Linked emails:
<table>
<tr ng-repeat="(address, info) in linkedEmails.linkedEmailList">
<td>{{address}}</td>
</tr>
</table>
</div>
<br/>
<h3>My rooms</h3>
<div class="rooms" ng-repeat="(rm_id, room) in rooms">
<div>
<a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_display_name }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
</div>
</div>
<br/>
<h3>Public rooms</h3>
<div class="public_rooms" ng-repeat="room in public_rooms">
<div>
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
</div>
</div>
<br/>
<div>
<form>
<input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
<input type="checkbox" ng-model="newRoom.private">private
<button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>
</form>
</div>
<div>
<form>
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
</form>
</div>
<br/>
{{ feedback }}
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More