Compare commits

...

154 Commits

Author SHA1 Message Date
Erik Johnston
d8a921f6a6 Add database upgrade script 2014-09-02 17:31:40 +01:00
Erik Johnston
47c7dd590d Un block quote list 2014-09-02 17:12:07 +01:00
Erik Johnston
9a7f7cb74f Try and remove annoying italics in github rst viewer 2014-09-02 17:10:28 +01:00
Erik Johnston
18f0247491 Remove an annoying 'the' 2014-09-02 17:07:07 +01:00
Erik Johnston
731c33dd97 Don't so heavily suggest you need to edit the default config. List the changed parameters. 2014-09-02 17:06:08 +01:00
Erik Johnston
c2c3ee8e4a Fix spelling 2014-09-02 17:01:04 +01:00
Erik Johnston
211a8b288a Hide the email textbox during registeration has it has not been wired up yet. 2014-09-02 16:59:45 +01:00
Erik Johnston
e166e29e87 Bump version and changelog 2014-09-02 16:57:10 +01:00
Erik Johnston
235f686da9 Update default endpoint port to match the default ports in the config 2014-09-02 16:56:57 +01:00
Erik Johnston
464e1fcfa5 Merge branch 'master' of github.com:matrix-org/synapse into release-v0.2.0
Conflicts:
	synapse/notifier.py
	webclient/room/room-controller.js
	webclient/room/room.html
2014-09-02 15:26:11 +01:00
Emmanuel ROHEE
e565a4bfc4 BF: Prevent controllers from being instantiated twice.
AngularJS will instantiate it when discovering ng-controller in their respective html files
2014-09-02 15:12:51 +02:00
Emmanuel ROHEE
d5da6b0cef Handle "m.room.create" in order to inform controllers about new rooms 2014-09-02 13:55:23 +02:00
Paul "LeoNerd" Evans
aa337f588c Order matters when adding REST servlets 2014-09-02 12:53:36 +01:00
Erik Johnston
4b8244fbf8 Fix bug where we didn't correctly store the ops power levels event. 2014-09-02 12:12:14 +01:00
Kegan Dougal
4ac80b8570 Whoops: Put /events parameters object in the right part of the api tree. 2014-09-02 12:09:47 +01:00
Kegan Dougal
5539251d82 Updated API docs with new keys on /createRoom and new API /rooms/$roomid/ban 2014-09-02 11:54:01 +01:00
David Baker
5c778f2f15 Add rest endpoint for the whole user profile 2014-09-02 11:49:17 +01:00
Kegan Dougal
fdcb876495 More ban spec. Added 'name' and 'topic' keys to /createRoom. 2014-09-02 11:46:53 +01:00
Paul "LeoNerd" Evans
d9d6fbb085 Safer handling of incoming presence EDUs that may or maynot contain 'presence' or 'state' 2014-09-02 11:17:43 +01:00
Paul "LeoNerd" Evans
7b56a7a3cb 'continue' to go to the next item in a for loop, not 'break' 2014-09-02 11:15:46 +01:00
Erik Johnston
10e7821461 By default, only room ops can change the name and topic. 2014-09-02 11:11:47 +01:00
Mark Haines
cf890e9d43 Remove option for disabling webclient because it was confusing 2014-09-02 11:09:41 +01:00
Erik Johnston
a808c06a10 Fix room permissions test 2014-09-02 11:03:03 +01:00
Erik Johnston
db02021aba Implement auth for kicking. 2014-09-02 10:58:33 +01:00
Erik Johnston
ed25abe05f Add message to assertion 2014-09-02 10:58:33 +01:00
Erik Johnston
08d2f902dd Default HTTP and HTTPS ports to 8008 and 8448 2014-09-02 10:58:33 +01:00
Erik Johnston
0393e87519 Make the demo listen on both HTTP and HTTPS ports 2014-09-02 10:58:33 +01:00
Mark Haines
45570e4695 os.makedirs is almost but not entirely unlike mkdir -p 2014-09-02 10:58:05 +01:00
Mark Haines
64b341cc10 Fix typo when reading TLS config 2014-09-02 10:54:56 +01:00
Emmanuel ROHEE
828101dd51 Represent user power level in a room by a red bar at the bottom of his avatar image. The width of this bar depends on the power level. 2014-09-02 11:54:35 +02:00
Paul "LeoNerd" Evans
7e22afbc7c Ensure that event source unit tests could cope with get_new_events_for_user() returning a Deferred 2014-09-02 10:53:16 +01:00
Mark Haines
30572e28c2 Make the config directory if the directory doesn't exisit when generating config 2014-09-02 10:52:25 +01:00
Mark Haines
d45f89c95b More helpful error messages for missing config 2014-09-02 10:49:11 +01:00
Kegan Dougal
ab0637c2c3 Yet more bullet points on various sections. 2014-09-02 10:41:00 +01:00
Emmanuel ROHEE
1bc05aef20 BF: Armed updateMemberListPresenceAge only once 2014-09-02 11:14:58 +02:00
David Baker
772c117e68 Make the intro bit a separate section rather than have a 'quick start' section that only tells you how to get started half way through. 2014-09-02 10:05:26 +01:00
Erik Johnston
040d985908 Add support for setting room name and topic when creating rooms 2014-09-02 10:02:14 +01:00
Kegan Dougal
15a7312273 Added section on banning users, currently in flux as the API changes / is implemented. 2014-09-02 09:34:28 +01:00
Emmanuel ROHEE
3122ff2433 Merge remote-tracking branch 'origin/develop' into develop 2014-09-02 10:20:47 +02:00
Emmanuel ROHEE
027857b261 Merge branch 'withoutYAML' into develop 2014-09-02 10:19:37 +02:00
Kegan Dougal
94bb4031f3 Expanded on bullet points in several sections. 2014-09-02 08:58:53 +01:00
Emmanuel ROHEE
07d609cbc2 BF: Take into account MEMBER_EVENT and PRESENCE_EVENT events only after the initialSync is done 2014-09-02 09:39:43 +02:00
Matthew Hodgson
68a04b9282 given everything's apparently gone https:// by default for c-s, change this hardcoding... 2014-09-01 23:28:43 +01:00
Mark Haines
399e004884 Add unsecure listener port to homeserver 2014-09-01 22:38:52 +01:00
Mark Haines
79650f795f enable ECDHE ciphers 2014-09-01 22:29:44 +01:00
Paul "LeoNerd" Evans
270d302834 Reënable presence visibility permission checking now it's not used on redundant code paths 2014-09-01 22:15:02 +01:00
Paul "LeoNerd" Evans
32fdf8efd6 No need to test is_presence_visible in _start_polling_local because we know it will be by when it's called 2014-09-01 22:15:02 +01:00
Matthew Hodgson
61e28cdb6f specify metavars in argparse 2014-09-01 22:11:08 +01:00
Mark Haines
118b250473 add pyyaml dependency for config files 2014-09-01 20:50:32 +01:00
Mark Haines
6fd730c96b Use yaml for config file 2014-09-01 20:44:51 +01:00
Erik Johnston
48142a01dd Don't set a 'default' key in the creation event 2014-09-01 20:39:43 +01:00
Mark Haines
8b69468e5f Use pregenerated DH params when generating config 2014-09-01 20:35:18 +01:00
Erik Johnston
bcfaaf7da6 That was a breaking db change. You need to recreate the databases. (In reality, it's enough to just run the im.sql through your db and change the schema version) 2014-09-01 20:34:07 +01:00
Erik Johnston
a85612baf8 Merge branch 'room_config' into develop 2014-09-01 20:28:11 +01:00
Erik Johnston
6d28560626 Don't put required power levels on permission state events 2014-09-01 20:27:45 +01:00
Mark Haines
86fa1138be Update readme with instructions on how to generate configs for homeservers 2014-09-01 20:22:16 +01:00
Erik Johnston
f452899fe2 Merge branch 'develop' of github.com:matrix-org/synapse into room_config 2014-09-01 19:57:28 +01:00
Erik Johnston
3f5ebccbff Fix the tests to include new db calls 2014-09-01 19:57:17 +01:00
Mark Haines
ff79437d9b Merge branch 'server2server_tls' into develop 2014-09-01 19:56:22 +01:00
Mark Haines
5452a8ee29 Fix SSL for federation http client 2014-09-01 18:43:08 +01:00
Mark Haines
00b042a3eb Merge remote-tracking branch 'origin/develop' into server2server_tls 2014-09-01 18:30:07 +01:00
Mark Haines
a53946a8a1 Enable SSL for s2s http client 2014-09-01 18:30:00 +01:00
Erik Johnston
b8ab9f1c0a Add all the necessary checks to make banning work. 2014-09-01 18:24:56 +01:00
Erik Johnston
3faa2ae78c Merge branch 'develop' of github.com:matrix-org/synapse into room_config 2014-09-01 18:21:29 +01:00
Emmanuel ROHEE
0271e8e692 Follow API renaming. state -> presence. mtime_ago -> last_active_ago 2014-09-01 19:09:49 +02:00
Kegan Dougal
74cffcf51a Small formatting errors 2014-09-01 17:57:44 +01:00
David Baker
6d07a28a29 fix lying docs 2014-09-01 17:55:41 +01:00
Mark Haines
6200630904 Add server TLS context factory 2014-09-01 17:55:35 +01:00
Emmanuel ROHEE
7d99cee3ef Added roomName, a directive to compute a room name based on its alias (not aliases for now) and its users displaynames or ids 2014-09-01 18:52:24 +02:00
Kegan Dougal
99ce820cc8 Little bit more on syncing APIs, need to know more about responses before more work can be done on it. 2014-09-01 17:51:31 +01:00
David Baker
ab8de33c76 Oops, show the voice call button once a call has ended 2014-09-01 17:41:42 +01:00
Paul "LeoNerd" Evans
32bfd567ac Added docs on presence 'last_active' field 2014-09-01 17:22:33 +01:00
David Baker
57f047a05a Fairly simple move of the call status widget to the header bar (and therefore into the index page rather than the rooms page). 2014-09-01 17:16:27 +01:00
Kegan Dougal
5a11a8ef69 Flesh out Room Events. 2014-09-01 16:52:33 +01:00
Emmanuel ROHEE
9b61076d42 BF: Check config exists (=defined in the localstorage) before using it 2014-09-01 17:43:27 +02:00
Mark Haines
46dcb0d890 Merge branch 'develop' into server2server_tls 2014-09-01 16:31:09 +01:00
Mark Haines
ef6a8e4f32 Listen using SSL 2014-09-01 16:30:43 +01:00
David Baker
b9172b982f missing semicolon 2014-09-01 16:23:13 +01:00
Paul "LeoNerd" Evans
1c6ab2d759 Bump a user's presence last_active time every time they send a message to a room 2014-09-01 16:16:35 +01:00
Paul "LeoNerd" Evans
59d3955db1 Have MemoryDataStore's get_rooms_for_user_where_membership_is() return room membership event objects, as per interface contract 2014-09-01 16:15:45 +01:00
Erik Johnston
db7109c43b Add beginnings of ban support. 2014-09-01 16:15:34 +01:00
Paul "LeoNerd" Evans
fd696f1243 Perform room unit tests with 'local' users actually in the right domain ;) 2014-09-01 15:58:30 +01:00
Kegan Dougal
401c16559d Specced out state/non-state events. Start working on the myriad of syncing APIs. 2014-09-01 15:58:20 +01:00
David Baker
fa6b3490e2 Handle call events in the recents view as well to get consistency with what it displays when refreshed. 2014-09-01 15:54:06 +01:00
David Baker
4e14e38bd5 Just show 'Call' for call events. Don't just splat out the content of the event for other events: it's rarely going to be sesnible. 2014-09-01 15:54:06 +01:00
Mark Haines
f5755bcadf Merge branch 'develop' into server2server_tls 2014-09-01 15:51:44 +01:00
Mark Haines
9ea1de432d Fix homeserver config parsing 2014-09-01 15:51:15 +01:00
Paul "LeoNerd" Evans
468d94c920 Rename API-visible 'mtime' presence field to 'last_active'; slightly different semantics 2014-09-01 15:39:50 +01:00
Emmanuel ROHEE
26a95988da Fixed presence change that occurs -Xs ago 2014-09-01 16:27:11 +02:00
Emmanuel ROHEE
c9ee9b45c7 Fixed presence state update. Specifically, the current user was sometimes indicated as offline 2014-09-01 16:22:59 +02:00
Paul "LeoNerd" Evans
02f4e3b3ff Rename 'state' presence key to the much more obvious 'presence'; maintain a legacy 'state' copy for now 2014-09-01 14:45:41 +01:00
Kegan Dougal
f500dd627a Fleshed out joining/leaving rooms. Added M_LIMIT_EXCEEDED standard error code for rate limited requests. 2014-09-01 14:44:25 +01:00
Erik Johnston
865469f233 Implement power level lists, default power levels and send_evnet_level/add_state_level events. 2014-09-01 14:39:09 +01:00
David Baker
67ffc00d48 Don't show the hang up button once tha call has ended 2014-09-01 14:15:32 +01:00
Kegan Dougal
389ee3624c Added /web folder which contains html/css (based off the default swagger-ui) for viewing the c-s json api. 2014-09-01 14:11:26 +01:00
Kegan Dougal
51b0b5c5ab Renamed json files because: paths. 2014-09-01 14:05:28 +01:00
Erik Johnston
10efca1a74 Merge branch 'develop' of github.com:matrix-org/synapse into room_config 2014-09-01 13:41:44 +01:00
Mark Haines
a9512d0994 Merge branch 'develop' into server2server_tls 2014-09-01 13:29:17 +01:00
Matthew Hodgson
fad58dbd08 add another public wishlist item 2014-09-01 13:22:50 +01:00
Matthew Hodgson
0b01c8560d make the little logos clickable for Erik 2014-09-01 13:22:42 +01:00
Kegan Dougal
3bb93abb34 Start digging into invite/join/leave sections. 2014-09-01 10:38:48 +01:00
Emmanuel ROHEE
f81002df60 BF: Pass desired_user_id to register API. And update the app header to avoid to display the previous matrix id 2014-09-01 11:12:24 +02:00
Matthew Hodgson
df752a15ce licenses, please... 2014-09-01 00:42:03 +01:00
Matthew Hodgson
d27e1ab148 fix footer on non-room pages 2014-09-01 00:41:53 +01:00
Mark Haines
3eb45eba0e Merge branch 'develop' into server2server_tls 2014-08-31 16:08:20 +01:00
Mark Haines
d9ebe531ed Add config tree to synapse. Add support for reading config from a file 2014-08-31 16:06:39 +01:00
Matthew Hodgson
7ca6d4e8f7 don't make HS ports explicit if it's the default for the protocol 2014-08-31 15:38:27 +01:00
Matthew Hodgson
2b7918bd6f missed a s#/matrix#/_matrix/g 2014-08-31 14:54:58 +01:00
Matthew Hodgson
8fe912d95c change the world: make the default matrix API URL prefix /_matrix rather than /matrix to make it easier for existing websites to mount a HS in their namespace without collisions.
perl -pi -e 's#/matrix#/_matrix#g' ./cmdclient/console.py ./docs/client-server/howto.rst ./docs/client-server/specification.rst ./docs/client-server/swagger_matrix/directory ./docs/client-server/swagger_matrix/events ./docs/client-server/swagger_matrix/login ./docs/client-server/swagger_matrix/presence ./docs/client-server/swagger_matrix/profile ./docs/client-server/swagger_matrix/registration ./docs/client-server/swagger_matrix/rooms ./docs/server-server/specification.rst ./graph/graph.py ./jsfiddles/create_room_send_msg/demo.js ./jsfiddles/event_stream/demo.js ./jsfiddles/example_app/demo.js ./jsfiddles/register_login/demo.js ./jsfiddles/room_memberships/demo.js ./synapse/api/urls.py ./tests/federation/test_federation.py ./tests/handlers/test_presence.py ./tests/handlers/test_typing.py ./tests/rest/test_events.py ./tests/rest/test_presence.py ./tests/rest/test_profile.py ./tests/rest/test_rooms.py ./webclient/components/fileUpload/file-upload-service.js ./webclient/components/matrix/matrix-service.js
2014-08-31 14:51:37 +01:00
Matthew Hodgson
820ef6e9d8 make lightbox a bit less ugly 2014-08-31 12:03:22 +01:00
Matthew Hodgson
0a65a2384c only show presence timings if we know them 2014-08-31 11:55:48 +01:00
Matthew Hodgson
1bc036a12d nasty big monolithic commit of a whole bunch of UI/UX improvements:
- add a simple CSS template across the app for navigation & cosmetics
 - split login into login & register, and totally reskin it
 - restructure room CSS to play nicely with it
 - implement basis 1:1 chat from user pages
 - disable autofocus on iOS to improve UX
2014-08-31 00:40:42 +01:00
Matthew Hodgson
b040bd6157 factor out mobile css into its own file 2014-08-31 00:38:45 +01:00
Matthew Hodgson
3ef312fb95 factor out the signup process into its own controller 2014-08-31 00:38:10 +01:00
Matthew Hodgson
91753655b7 moar logos! 2014-08-31 00:37:47 +01:00
Matthew Hodgson
17a4bc10bc switch default profile image to one of our own devising (dating back to communicator(!)) 2014-08-31 00:37:01 +01:00
Matthew Hodgson
885e0c8b76 add a basic wishlist 2014-08-31 00:36:11 +01:00
Matthew Hodgson
2a0e79bbfa tweaks to settings page 2014-08-30 00:50:51 +01:00
Matthew Hodgson
f64ce52305 actually add a 'home' button 2014-08-30 00:44:56 +01:00
Matthew Hodgson
c715660cb8 shrink text bubble font a bit; make image thumbnails always fit in their bubbles 2014-08-30 00:41:36 +01:00
Paul "LeoNerd" Evans
93407cf7cf Bugfixes on presence pushes on user joining:
* No need to inform clients of status of remote users; as that will
   arrive in due course anyway. We don't -have- the state currently, so
   we'd only send an unknown message

 * Remember to bump the presence serial for the event source, so the
   notifiers will wake up and report it
2014-08-29 19:53:33 +01:00
Paul "LeoNerd" Evans
a8e8d1d06c Renamed get_current_token_part to get_current_key 2014-08-29 19:15:23 +01:00
Paul "LeoNerd" Evans
eec67a675f Have EventSource's get_new_events_for_user() API work only on keys within that source, not overall eventstream tokens 2014-08-29 19:13:55 +01:00
Paul "LeoNerd" Evans
56424eca5c Bugfix for rest presence test - datastore needs to implement profile methods 2014-08-29 19:12:22 +01:00
Paul "LeoNerd" Evans
6797c7f1b1 TypingNotificationEventSource has to return proper tokens, not int 0 2014-08-29 19:00:55 +01:00
Paul "LeoNerd" Evans
4bfdec1eb2 Rename 'events_key' to 'room_key' so it matches the name of the event source 2014-08-29 19:00:55 +01:00
David Baker
7b79c0f08f v0.1.2 2014-08-29 18:13:34 +01:00
David Baker
8c36179d35 Give basic feedback on the state of VoIP calls in the UI (manually adding to master having added to develop) 2014-08-29 18:11:20 +01:00
David Baker
490f142d73 Give basic feedback on the state of VoIP calls in the UI. 2014-08-29 18:01:01 +01:00
Matthew Hodgson
26766c22eb todo 2014-08-29 17:54:18 +01:00
Matthew Hodgson
e006f101c3 fix mobile skin by hiding recents LHS in /rooms 2014-08-29 17:54:18 +01:00
Kegan Dougal
74cc722b96 Added case-sensitivity notes on IDs, added TODO on UTF-8. 2014-08-29 17:45:09 +01:00
Paul "LeoNerd" Evans
6dd50da54e Define a new event stream data source for typing notifications (currently null) 2014-08-29 17:39:33 +01:00
Paul "LeoNerd" Evans
f85a3757cf Avoid hardcoding names of individual stream token keys in its own implementation; this at least reduces the number of places in source code the individual parts are stored 2014-08-29 17:31:39 +01:00
Emmanuel ROHEE
95cbd026cc oops. Should not have c+p all lines 2014-08-29 18:29:04 +02:00
Emmanuel ROHEE
e1f249ce20 Implemented /nick 2014-08-29 18:24:13 +02:00
Emmanuel ROHEE
67f42b2f26 Get user display name and avatar from the server rather than storing them in the local storage 2014-08-29 18:23:18 +02:00
Matthew Hodgson
b86d2a2d4f update presence times in realtime through the magic of two-way binding 2014-08-29 17:22:48 +01:00
Paul "LeoNerd" Evans
d4145abd33 Use str.join() properly 2014-08-29 17:17:11 +01:00
Paul "LeoNerd" Evans
20d0db6cfb Move the *EventSource classes into the handlers they relate to, so it's easier to find the code 2014-08-29 17:10:28 +01:00
Kegan Dougal
ca025c2b1d Rooms: More subsections, more bullet points, more detail. 2014-08-29 16:50:41 +01:00
Emmanuel ROHEE
c3a774e414 Show desktop notification state. Provide help if the user has previously denied permission to display them. 2014-08-29 17:13:57 +02:00
David Baker
0ef54caa28 better support for call hangups 2014-08-29 15:18:57 +01:00
Erik Johnston
1118f02689 Start adding storage for new events. 2014-08-29 15:18:30 +01:00
Kegan Dougal
1cdc29e260 Fix JSFiddles to work with the new C-S API. 2014-08-29 15:01:55 +01:00
Kegan Dougal
339dd3dc6c Update API swagger JSON to default to POSTs not PUTs when describing the API. 2014-08-29 15:01:55 +01:00
Emmanuel ROHEE
27047d8f51 Merge branch 'release-v0.1.0' into develop 2014-08-29 15:33:17 +02:00
Emmanuel ROHEE
7c4b47652e The away state is unavailable not offline 2014-08-29 15:33:02 +02:00
Erik Johnston
8e2d4c6da5 Merge branch 'develop' of github.com:matrix-org/synapse into room_config 2014-08-29 14:22:04 +01:00
David Baker
e76cd252fe Merge branch 'release-v0.1.0' into develop 2014-08-29 14:01:30 +01:00
Erik Johnston
fbdacce3fe Merge branch 'develop' of github.com:matrix-org/synapse into develop 2014-08-29 13:52:10 +01:00
Kegan Dougal
898dde8812 Flesh out API standards. 2014-08-29 13:50:31 +01:00
Mark Haines
d7ae9b90a0 Add store for server certificates and keys 2014-08-28 18:19:47 +01:00
Erik Johnston
d2bc5d6f29 Create the correct events with the right configuration when creating a new room. 2014-08-28 10:59:15 +01:00
120 changed files with 14604 additions and 1468 deletions

View File

@@ -1,3 +1,31 @@
Changes in synapse 0.2.0 (2014-09-02)
=====================================
This update changes many configuration options, updates the
database schema and mandates SSL for server-server connections.
Homeserver:
* Require SSL for server-server connections.
* Add SSL listener for client-server connections.
* Add ability to use config files.
* Add support for kicking/banning and power levels.
* Allow setting of room names and topics on creation.
* Change presence to include last seen time of the user.
* Change url path prefix to /_matrix/...
* Bug fixes to presence.
Webclient:
* Reskin the CSS for registration and login.
* Various improvements to rooms CSS.
* Support changes in client-server API.
* Bug fixes to VOIP UI.
* Various bug fixes to handling of changes to room member list.
Changes in synapse 0.1.2 (2014-08-29)
=====================================
Webclient:
* Add basic call state UI for VoIP calls.
Changes in synapse 0.1.1 (2014-08-29)
=====================================

View File

@@ -1,5 +1,5 @@
Quick Start
===========
Introduction
============
Matrix is an ambitious new ecosystem for open federated Instant Messaging and
VoIP[1]. The basics you need to know to get up and running are:
@@ -17,6 +17,9 @@ The overall architecture is::
client <----> homeserver <=================> homeserver <-----> client
e.g. matrix.org:8080 e.g. mydomain.net:8080
Quick Start
===========
To get up and running:
- To simply play with an **existing** homeserver you can
@@ -173,7 +176,11 @@ IDs:
For the first form, simply pass the required hostname (of the machine) as the
--host parameter::
$ python synapse/app/homeserver.py --host machine.my.domain.name
$ python synapse/app/homeserver.py \
--server-name machine.my.domain.name \
--config-path homeserver.config \
--generate-config
$ python synapse/app/homeserver.py --config-path homeserver.config
For the second form, first create your SRV record and publish it in DNS. This
needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname
@@ -186,7 +193,13 @@ record would then look something like::
At this point, you should then run the homeserver with the hostname of this
SRV record, as that is the name other machines will expect it to have::
$ python synapse/app/homeserver.py --host my.domain.name --port 8448
$ python synapse/app/homeserver.py \
--server-name YOURDOMAIN \
--bind-port 8448 \
--config-path homeserver.config \
--generate-config
$ python synapse/app/homeserver.py --config-path homeserver.config
You may additionally want to pass one or more "-v" options, in order to
increase the verbosity of logging output; at least for initial testing.

View File

@@ -1,3 +1,34 @@
Upgrading to v0.2.0
===================
To upgrade the database schema, run::
./database-prepare-for-0.2.0.sh "<database>.db"
The home server now requires setting up of SSL config before it can run. To
automatically generate default config use::
$ python synapse/app/homeserver.py \
--server-name machine.my.domain.name \
--bind-port 8448 \
--config-path homeserver.config \
--generate-config
This config can be edited if desired, for example to specify a different SSL
certificate to use. Once done you can run the home server using::
$ python synapse/app/homeserver.py --config-path homeserver.config
See the README.rst for more information.
Also note that some config options have been renamed, including:
- "host" to "server-name"
- "database" to "database-path"
- "port" to "bind-port" and "unsecure-port"
Upgrading to v0.0.1
===================

View File

@@ -1 +1 @@
0.1.1
0.2.0

7
WISHLIST.rst Normal file
View File

@@ -0,0 +1,7 @@
Broad-sweeping stuff which would be nice to have
================================================
- Additional SQL backends beyond sqlite
- homeserver implementation in go
- homeserver implementation in node.js
- client SDKs

View File

@@ -60,7 +60,7 @@ class SynapseCmd(cmd.Cmd):
"complete_usernames": "on",
"send_delivery_receipts": "on"
}
self.path_prefix = "/matrix/client/api/v1"
self.path_prefix = "/_matrix/client/api/v1"
self.event_stream_token = "END"
self.prompt = ">>> "
@@ -252,7 +252,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks
def _do_emailrequest(self, args):
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/requestToken"
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']})
@@ -274,7 +274,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks
def _do_emailvalidate(self, args):
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/submitToken"
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']})
@@ -294,7 +294,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks
def _do_3pidbind(self, args):
url = self._identityServerUrl()+"/matrix/identity/api/v1/3pid/bind"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/3pid/bind"
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
headers={'Content-Type': ['application/x-www-form-urlencoded']})
@@ -360,14 +360,14 @@ class SynapseCmd(cmd.Cmd):
def _do_invite(self, roomid, userstring):
if (not userstring.startswith('@') and
self._is_on("complete_usernames")):
url = self._identityServerUrl()+"/matrix/identity/api/v1/lookup"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/lookup"
json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring})
mxid = None
if 'mxid' in json_res and 'signatures' in json_res:
url = self._identityServerUrl()+"/matrix/identity/api/v1/pubkey/ed25519"
url = self._identityServerUrl()+"/_matrix/identity/api/v1/pubkey/ed25519"
pubKey = None
pubKeyObj = yield self.http_client.do_request("GET", url)

10
database-prepare-for-0.2.0.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
# This is will prepare a synapse database for running with v0.2.0 of synapse.
set -e
cp "$1" "$1.bak"
sqlite3 "$1" < "synapse/storage/schema/im.sql"
sqlite3 "$1" <<< "PRAGMA user_version = 2;"

9
demo/demo.tls.dh Normal file
View File

@@ -0,0 +1,9 @@
2048-bit DH parameters taken from rfc3526
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb
IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft
awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT
mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh
fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq
5RXSJhiY+gUQFXKOWoqsqmj//////////wIBAg==
-----END DH PARAMETERS-----

View File

@@ -6,17 +6,29 @@ CWD=$(pwd)
cd "$DIR/.."
mkdir -p demo/etc
for port in 8080 8081 8082; do
echo "Starting server on port $port... "
https_port=$((port + 400))
python -m synapse.app.homeserver \
-p "$port" \
-H "localhost:$port" \
--generate-config \
--config-path "demo/etc/$port.config" \
-p "$https_port" \
--unsecure-port "$port" \
-H "localhost:$https_port" \
-f "$DIR/$port.log" \
-d "$DIR/$port.db" \
-vv \
-D --pid-file "$DIR/$port.pid" \
--manhole $((port + 1000))
--manhole $((port + 1000)) \
--tls-dh-params-path "demo/demo.tls.dh"
python -m synapse.app.homeserver \
--config-path "demo/etc/$port.config" \
-vv \
done
echo "Starting webclient on port 8000..."

View File

@@ -30,7 +30,7 @@ Registration
The aim of registration is to get a user ID and access token which you will need
when accessing other APIs::
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/register"
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/_matrix/client/api/v1/register"
{
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",
@@ -51,13 +51,13 @@ Login
-----
The aim when logging in is to get an access token for your existing user ID::
curl -XGET "http://localhost:8080/matrix/client/api/v1/login"
curl -XGET "http://localhost:8080/_matrix/client/api/v1/login"
{
"type": "m.login.password"
}
curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/login"
curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/_matrix/client/api/v1/login"
{
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",
@@ -87,7 +87,7 @@ Creating a room
If you want to send a message to someone, you have to be in a room with them. To
create a room::
curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/_matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
{
"room_alias": "#tutorial:localhost",
@@ -105,7 +105,7 @@ Sending messages
----------------
You can now send messages to this room::
curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
NB: There are no limitations to the types of messages which can be exchanged.
The only requirement is that ``"msgtype"`` is specified.
@@ -127,7 +127,7 @@ Inviting a user to a room
-------------------------
You can directly invite a user to a room like so::
curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
This informs ``@myfriend:localhost`` of the room ID
``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room.
@@ -137,7 +137,7 @@ Joining a room via an invite
If you receive an invite, you can join the room by changing the membership to
join::
curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
NB: Only the person invited (``@myfriend:localhost``) can change the membership
state to ``"join"``.
@@ -147,7 +147,7 @@ Joining a room via an alias
Alternatively, if you know the room alias for this room and the room config
allows it, you can directly join a room via the alias::
curl -XPUT -d '{}' "http://localhost:8080/matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
curl -XPUT -d '{}' "http://localhost:8080/_matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
{
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
@@ -173,7 +173,7 @@ Getting all state
If the client doesn't know any information on the rooms the user is
invited/joined on, they can get all the user's state for all rooms::
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
curl -XGET "http://localhost:8080/_matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
[
{
@@ -236,7 +236,7 @@ all of the messages and feedback for these rooms. This can be a LOT of data. You
may just want the most recent message for each room. This can be achieved by
applying pagination stream parameters to this request::
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
curl -XGET "http://localhost:8080/_matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
[
{
@@ -271,7 +271,7 @@ Getting live state
Once you know which rooms the client has previously interacted with, you need to
listen for incoming events. This can be done like so::
curl -XGET "http://localhost:8080/matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
curl -XGET "http://localhost:8080/_matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
{
"chunk": [],

View File

@@ -306,11 +306,11 @@ POST requests MUST be submitted as application/json.
All paths MUST be namespaced by the version of the API being used. This should
be:
/matrix/client/api/v1
/_matrix/client/api/v1
All REST paths in this section MUST be prefixed with this. E.g.
REST Path: /rooms/$room_id
Absolute Path: /matrix/client/api/v1/rooms/$room_id
Absolute Path: /_matrix/client/api/v1/rooms/$room_id
Registration
============
@@ -1007,26 +1007,15 @@ for users from other servers entirely.
Presence
========
In the following messages, the presence state is an integer enumeration of the
following states:
0 : OFFLINE
1 : BUSY
2 : ONLINE
3 : FREE_TO_CHAT
Aside from OFFLINE, the protocol doesn't assign any special meaning to these
states; they are provided as an approximate signal for users to give to other
users and for clients to present them in some way that may be useful. Clients
could have different behaviours for different states of the user's presence, for
example to decide how much prominence or sound to use for incoming event
notifications.
In the following messages, the presence state is a presence string as described in
the main specification document.
Getting/Setting your own presence state
---------------------------------------
REST Path: /presence/$user_id/status
Valid methods: GET/PUT
Required keys:
state : [0|1|2|3] - The user's new presence state
presence : <string> - The user's new presence state
Optional keys:
status_msg : text string provided by the user to explain their status
@@ -1039,7 +1028,7 @@ Fetching your presence list
following keys:
{
"user_id" : string giving the observed user's ID
"state" : int giving their status
"presence" : int giving their status
"status_msg" : optional text string
"displayname" : optional text string from the user's profile
"avatar_url" : optional text string from the user's profile

View File

@@ -3,31 +3,31 @@
"swaggerVersion": "1.2",
"apis": [
{
"path": "/login",
"path": "-login",
"description": "Login operations"
},
{
"path": "/registration",
"path": "-registration",
"description": "Registration operations"
},
{
"path": "/rooms",
"path": "-rooms",
"description": "Room operations"
},
{
"path": "/profile",
"path": "-profile",
"description": "Profile operations"
},
{
"path": "/presence",
"path": "-presence",
"description": "Presence operations"
},
{
"path": "/events",
"path": "-events",
"description": "Event operations"
},
{
"path": "/directory",
"path": "-directory",
"description": "Directory operations"
}
],

View File

@@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/directory",
"produces": [
"application/json"

View File

@@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/events",
"produces": [
"application/json"
@@ -15,25 +15,26 @@
"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"
}
],
"parameters": [
{
"name": "from",
"description": "The token to stream from.",
"required": false,
"type": "string",
"paramType": "query"
},
{
"name": "timeout",
"description": "The maximum time in milliseconds to wait for an event.",
"required": false,
"type": "integer",
"paramType": "query"
"nickname": "get_event_stream",
"parameters": [
{
"name": "from",
"description": "The token to stream from.",
"required": false,
"type": "string",
"paramType": "query"
},
{
"name": "timeout",
"description": "The maximum time in milliseconds to wait for an event.",
"required": false,
"type": "integer",
"paramType": "query"
}
]
}
],
"responseMessages": [
{
"code": 400,

View File

@@ -40,7 +40,7 @@
"path": "/login"
}
],
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"consumes": [
"application/json"
],

View File

@@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/presence",
"produces": [
"application/json"

View File

@@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/profile",
"produces": [
"application/json"

View File

@@ -37,7 +37,7 @@
"path": "/register"
}
],
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"consumes": [
"application/json"
],

View File

@@ -1,7 +1,7 @@
{
"apiVersion": "1.0.0",
"swaggerVersion": "1.2",
"basePath": "http://localhost:8080/matrix/client/api/v1",
"basePath": "http://localhost:8080/_matrix/client/api/v1",
"resourcePath": "/rooms",
"produces": [
"application/json"
@@ -14,12 +14,12 @@
},
"apis": [
{
"path": "/rooms/{roomId}/send/{eventType}/{txnId}",
"path": "/rooms/{roomId}/send/{eventType}",
"operations": [
{
"method": "PUT",
"method": "POST",
"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}",
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
"type": "EventId",
"nickname": "send_non_state_event",
"consumes": [
@@ -46,13 +46,6 @@
"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"
}
]
}
@@ -104,12 +97,12 @@
]
},
{
"path": "/rooms/{roomId}/send/m.room.message/{txnId}",
"path": "/rooms/{roomId}/send/m.room.message",
"operations": [
{
"method": "PUT",
"method": "POST",
"summary": "Send a message in this room.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message",
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
"type": "EventId",
"nickname": "send_message",
"consumes": [
@@ -129,13 +122,6 @@
"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"
}
]
}
@@ -195,12 +181,12 @@
]
},
{
"path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}",
"path": "/rooms/{roomId}/send/m.room.message.feedback",
"operations": [
{
"method": "PUT",
"method": "POST",
"summary": "Send feedback to a message.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback",
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
"type": "EventId",
"nickname": "send_feedback",
"consumes": [
@@ -220,13 +206,6 @@
"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"
}
],
"responseMessages": [
@@ -239,12 +218,12 @@
]
},
{
"path": "/rooms/{roomId}/invite/{txnId}",
"path": "/rooms/{roomId}/invite",
"operations": [
{
"method": "PUT",
"method": "POST",
"summary": "Invite a user to this room.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/invite",
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
"type": "void",
"nickname": "invite",
"consumes": [
@@ -258,13 +237,6 @@
"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.",
@@ -277,12 +249,12 @@
]
},
{
"path": "/rooms/{roomId}/join/{txnId}",
"path": "/rooms/{roomId}/join",
"operations": [
{
"method": "PUT",
"method": "POST",
"summary": "Join this room.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/join",
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
"type": "void",
"nickname": "join_room",
"consumes": [
@@ -295,25 +267,18 @@
"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}",
"path": "/rooms/{roomId}/leave",
"operations": [
{
"method": "PUT",
"method": "POST",
"summary": "Leave this room.",
"notes": "This operation can also be done as a POST to /rooms/{roomId}/leave",
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
"type": "void",
"nickname": "leave",
"consumes": [
@@ -326,13 +291,37 @@
"required": true,
"type": "string",
"paramType": "path"
},
}
]
}
]
},
{
"path": "/rooms/{roomId}/ban",
"operations": [
{
"method": "POST",
"summary": "Ban a user in the room.",
"notes": "This operation can also be done as a PUT by suffixing /{txnId}. The caller must have the required power level to do this operation.",
"type": "void",
"nickname": "ban",
"consumes": [
"application/json"
],
"parameters": [
{
"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,
"name": "roomId",
"description": "The room which has the user to ban.",
"required": true,
"type": "string",
"paramType": "path"
},
{
"name": "body",
"description": "The user to ban.",
"required": true,
"type": "BanRequest",
"paramType": "body"
}
]
}
@@ -476,7 +465,7 @@
"parameters": [
{
"name": "body",
"description": "The desired configuration for the room.",
"description": "The desired configuration for the room. This operation can also be done as a PUT by suffixing /{txnId}.",
"required": true,
"type": "RoomConfig",
"paramType": "body"
@@ -699,6 +688,16 @@
"type": "string",
"description": "The alias to give the new room.",
"required": false
},
"name": {
"type": "string",
"description": "Sets the name of the room. Send a m.room.name event after creating the room with the 'name' key specified.",
"required": false
},
"topic": {
"type": "string",
"description": "Sets the topic for the room. Send a m.room.topic event after creating the room with the 'topic' key specified.",
"required": false
}
}
},
@@ -831,6 +830,19 @@
}
}
},
"BanRequest": {
"id": "BanRequest",
"properties": {
"user_id": {
"type": "string",
"description": "The fully-qualified user ID."
},
"reason": {
"type": "string",
"description": "The reason for the ban."
}
}
},
"InitialSyncRoomData": {
"id": "InitialSyncRoomData",
"properties": {

View File

@@ -0,0 +1,5 @@
To get this running:
ln -s ../swagger_matrix
python -m SimpleHTTPServer
Go to http://localhost:8000/swagger.html

View File

@@ -0,0 +1,38 @@
// Backbone.js 0.9.2
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);

View File

@@ -0,0 +1,16 @@
/* latin */
@font-face {
font-family: 'Droid Sans';
font-style: normal;
font-weight: 400;
src: local('Droid Sans'), local('DroidSans'), url(http://fonts.gstatic.com/s/droidsans/v5/s-BiyweUPV0v-yRb-cjciPk_vArhqVIZ0nv9q090hN8.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
/* latin */
@font-face {
font-family: 'Droid Sans';
font-style: normal;
font-weight: 700;
src: local('Droid Sans Bold'), local('DroidSans-Bold'), url(http://fonts.gstatic.com/s/droidsans/v5/EFpQQyG9GqCrobXxL-KRMYWiMMZ7xLd792ULpGE4W_Y.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
/*
* jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010
* http://benalman.com/projects/jquery-bbq-plugin/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
(function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M<N?O[P]||(R[M+1]&&isNaN(R[M+1])?{}:[]):J}}else{if($.isArray(H[P])){H[P].push(J)}else{if(H[P]!==i){H[P]=[H[P],J]}else{H[P]=J}}}}else{if(P){H[P]=F?i:""}}});return H};function z(H,F,G){if(F===i||typeof F==="boolean"){G=F;F=a[H?D:A]()}else{F=E(F)?F.replace(H?w:x,""):F}return l(F,G)}l[A]=B(z,0);l[D]=v=B(z,1);$[y]||($[y]=function(F){return $.extend(C,F)})({a:k,base:k,iframe:t,img:t,input:t,form:"action",link:k,script:t});j=$[y];function s(I,G,H,F){if(!E(H)&&typeof H!=="object"){F=H;H=G;G=i}return this.each(function(){var L=$(this),J=G||j()[(this.nodeName||"").toLowerCase()]||"",K=J&&L.attr(J)||"";L.attr(J,a[I](K,H,F))})}$.fn[A]=B(s,A);$.fn[D]=B(s,D);b.pushState=q=function(I,F){if(E(I)&&/^#/.test(I)&&F===i){F=2}var H=I!==i,G=c(p[g][k],H?I:{},H?F:2);p[g][k]=G+(/#/.test(G)?"":"#")};b.getState=u=function(F,G){return F===i||typeof F==="boolean"?v(F):v(G)[F]};b.removeState=function(F){var G={};if(F!==i){G=u();$.each($.isArray(F)?F:arguments,function(I,H){delete G[H]})}q(G,2)};e[d]=$.extend(e[d],{add:function(F){var H;function G(J){var I=J[D]=c();J.getState=function(K,L){return K===i||typeof K==="boolean"?l(I,K):l(I,L)[K]};H.apply(this,arguments)}if($.isFunction(F)){H=F;return G}else{H=F.handler;F.handler=G}}})})(jQuery,this);
/*
* jQuery hashchange event - v1.2 - 2/11/2010
* http://benalman.com/projects/jquery-hashchange-plugin/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
(function($,i,b){var j,k=$.event.special,c="location",d="hashchange",l="href",f=$.browser,g=document.documentMode,h=f.msie&&(g===b||g<8),e="on"+d in i&&!h;function a(m){m=m||i[c][l];return m.replace(/^[^#]*#?(.*)$/,"$1")}$[d+"Delay"]=100;k[d]=$.extend(k[d],{setup:function(){if(e){return false}$(j.start)},teardown:function(){if(e){return false}$(j.stop)}});j=(function(){var m={},r,n,o,q;function p(){o=q=function(s){return s};if(h){n=$('<iframe src="javascript:0"/>').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this);

View File

@@ -0,0 +1 @@
(function(b){b.fn.slideto=function(a){a=b.extend({slide_duration:"slow",highlight_duration:3E3,highlight:true,highlight_color:"#FFFF99"},a);return this.each(function(){obj=b(this);b("body").animate({scrollTop:obj.offset().top},a.slide_duration,function(){a.highlight&&b.ui.version&&obj.effect("highlight",{color:a.highlight_color},a.highlight_duration)})})}})(jQuery);

View File

@@ -0,0 +1,8 @@
/*
jQuery Wiggle
Author: WonderGroup, Jordan Thomas
URL: http://labs.wondergroup.com/demos/mini-ui/index.html
License: MIT (http://en.wikipedia.org/wiki/MIT_License)
*/
jQuery.fn.wiggle=function(o){var d={speed:50,wiggles:3,travel:5,callback:null};var o=jQuery.extend(d,o);return this.each(function(){var cache=this;var wrap=jQuery(this).wrap('<div class="wiggle-wrap"></div>').css("position","relative");var calls=0;for(i=1;i<=o.wiggles;i++){jQuery(this).animate({left:"-="+o.travel},o.speed).animate({left:"+="+o.travel*2},o.speed*2).animate({left:"-="+o.travel},o.speed,function(){calls++;if(jQuery(cache).parent().hasClass('wiggle-wrap')){jQuery(cache).parent().replaceWith(cache);}
if(calls==o.wiggles&&jQuery.isFunction(o.callback)){o.callback();}});}});};

View File

@@ -0,0 +1,125 @@
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
var appName;
var popupMask;
var popupDialog;
var clientId;
var realm;
function handleLogin() {
var scopes = [];
if(window.swaggerUi.api.authSchemes
&& window.swaggerUi.api.authSchemes.oauth2
&& window.swaggerUi.api.authSchemes.oauth2.scopes) {
scopes = window.swaggerUi.api.authSchemes.oauth2.scopes;
}
if(window.swaggerUi.api
&& window.swaggerUi.api.info) {
appName = window.swaggerUi.api.info.title;
}
if(popupDialog.length > 0)
popupDialog = popupDialog.last();
else {
popupDialog = $(
[
'<div class="api-popup-dialog">',
'<div class="api-popup-title">Select OAuth2.0 Scopes</div>',
'<div class="api-popup-content">',
'<p>Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.',
'<a href="#">Learn how to use</a>',
'</p>',
'<p><strong>' + appName + '</strong> API requires the following scopes. Select which ones you want to grant to Swagger UI.</p>',
'<ul class="api-popup-scopes">',
'</ul>',
'<p class="error-msg"></p>',
'<div class="api-popup-actions"><button class="api-popup-authbtn api-button green" type="button">Authorize</button><button class="api-popup-cancel api-button gray" type="button">Cancel</button></div>',
'</div>',
'</div>'].join(''));
$(document.body).append(popupDialog);
popup = popupDialog.find('ul.api-popup-scopes').empty();
for (i = 0; i < scopes.length; i ++) {
scope = scopes[i];
str = '<li><input type="checkbox" id="scope_' + i + '" scope="' + scope.scope + '"/>' + '<label for="scope_' + i + '">' + scope.scope;
if (scope.description) {
str += '<br/><span class="api-scope-desc">' + scope.description + '</span>';
}
str += '</label></li>';
popup.append(str);
}
}
var $win = $(window),
dw = $win.width(),
dh = $win.height(),
st = $win.scrollTop(),
dlgWd = popupDialog.outerWidth(),
dlgHt = popupDialog.outerHeight(),
top = (dh -dlgHt)/2 + st,
left = (dw - dlgWd)/2;
popupDialog.css({
top: (top < 0? 0 : top) + 'px',
left: (left < 0? 0 : left) + 'px'
});
popupDialog.find('button.api-popup-cancel').click(function() {
popupMask.hide();
popupDialog.hide();
});
popupDialog.find('button.api-popup-authbtn').click(function() {
popupMask.hide();
popupDialog.hide();
var authSchemes = window.swaggerUi.api.authSchemes;
var host = window.location;
var redirectUrl = host.protocol + '//' + host.host + "/o2c.html";
var url = null;
var p = window.swaggerUi.api.authSchemes;
for (var key in p) {
if (p.hasOwnProperty(key)) {
var o = p[key].grantTypes;
for(var t in o) {
if(o.hasOwnProperty(t) && t === 'implicit') {
var dets = o[t];
url = dets.loginEndpoint.url + "?response_type=token";
window.swaggerUi.tokenName = dets.tokenName;
}
}
}
}
var scopes = []
var o = $('.api-popup-scopes').find('input:checked');
for(k =0; k < o.length; k++) {
scopes.push($(o[k]).attr("scope"));
}
window.enabledScopes=scopes;
url += '&redirect_uri=' + encodeURIComponent(redirectUrl);
url += '&realm=' + encodeURIComponent(realm);
url += '&client_id=' + encodeURIComponent(clientId);
url += '&scope=' + encodeURIComponent(scopes);
window.open(url);
});
popupMask.show();
popupDialog.show();
return;
}
function handleLogout() {
for(key in window.authorizations.authz){
window.authorizations.remove(key)
}
window.enabledScopes = null;
$('.api-ic.ic-on').addClass('ic-off');
$('.api-ic.ic-on').removeClass('ic-on');
// set the info box
$('.api-ic.ic-warning').addClass('ic-error');
$('.api-ic.ic-warning').removeClass('ic-warning');
}
function initOAuth(opts) {
var o = (opts||{});
var errors = [];
appName = (o.appName||errors.push("missing appName"));
popupMask = (o.popupMask||$('#api-common-mask'));
popupDialog = (o.popupDialog||$('.api-popup-dialog'));
clientId = (o.clientId||errors.push("missing client id"));
realm = (o.realm||errors.push("missing realm"));
if(errors.length > 0){
log("auth unable initialize oauth: " + errors);
return;
}
$('pre code').each(function(i, e) {hljs.highlightBlock(e)});
$('.api-ic').click(function(s) {
if($(s.target).hasClass('ic-off'))
handleLogin();
else {
handleLogout();
}
false;
});
}
function onOAuthComplete(token) {
if(token) {
if(token.error) {
var checkbox = $('input[type=checkbox],.secured')
checkbox.each(function(pos){
checkbox[pos].checked = false;
});
alert(token.error);
}
else {
var b = token[window.swaggerUi.tokenName];
if(b){
// if all roles are satisfied
var o = null;
$.each($('.auth #api_information_panel'), function(k, v) {
var children = v;
if(children && children.childNodes) {
var requiredScopes = [];
$.each((children.childNodes), function (k1, v1){
var inner = v1.innerHTML;
if(inner)
requiredScopes.push(inner);
});
var diff = [];
for(var i=0; i < requiredScopes.length; i++) {
var s = requiredScopes[i];
if(window.enabledScopes && window.enabledScopes.indexOf(s) == -1) {
diff.push(s);
}
}
if(diff.length > 0){
o = v.parentNode;
$(o.parentNode).find('.api-ic.ic-on').addClass('ic-off');
$(o.parentNode).find('.api-ic.ic-on').removeClass('ic-on');
// sorry, not all scopes are satisfied
$(o).find('.api-ic').addClass('ic-warning');
$(o).find('.api-ic').removeClass('ic-error');
}
else {
o = v.parentNode;
$(o.parentNode).find('.api-ic.ic-off').addClass('ic-on');
$(o.parentNode).find('.api-ic.ic-off').removeClass('ic-off');
// all scopes are satisfied
$(o).find('.api-ic').addClass('ic-info');
$(o).find('.api-ic').removeClass('ic-warning');
$(o).find('.api-ic').removeClass('ic-error');
}
}
});
window.authorizations.add("oauth2", new ApiKeyAuthorization("Authorization", "Bearer " + b, "header"));
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
// Underscore.js 1.3.3
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Matrix Client-Server API Documentation</title>
<link href="./files/css" rel="stylesheet" type="text/css">
<link href="./files/reset.css" media="screen" rel="stylesheet" type="text/css">
<link href="./files/screen.css" media="screen" rel="stylesheet" type="text/css">
<link href="./files/reset.css" media="print" rel="stylesheet" type="text/css">
<link href="./files/screen.css" media="print" rel="stylesheet" type="text/css">
<script type="text/javascript" src="./files/shred.bundle.js"></script>
<script src="./files/jquery-1.8.0.min.js" type="text/javascript"></script>
<script src="./files/jquery.slideto.min.js" type="text/javascript"></script>
<script src="./files/jquery.wiggle.min.js" type="text/javascript"></script>
<script src="./files/jquery.ba-bbq.min.js" type="text/javascript"></script>
<script src="./files/handlebars-1.0.0.js" type="text/javascript"></script>
<script src="./files/underscore-min.js" type="text/javascript"></script>
<script src="./files/backbone-min.js" type="text/javascript"></script>
<script src="./files/swagger.js" type="text/javascript"></script>
<script src="./files/swagger-ui.js" type="text/javascript"></script>
<script src="./files/highlight.7.3.pack.js" type="text/javascript"></script>
<!-- enabling this will enable oauth2 implicit scope support -->
<script src="./files/swagger-oauth.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
window.swaggerUi = new SwaggerUi({
url: "http://localhost:8000/swagger_matrix/api-docs",
dom_id: "swagger-ui-container",
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
onComplete: function(swaggerApi, swaggerUi){
log("Loaded SwaggerUI");
if(typeof initOAuth == "function") {
initOAuth({
clientId: "your-client-id",
realm: "your-realms",
appName: "your-app-name"
});
}
$('pre code').each(function(i, e) {
hljs.highlightBlock(e)
});
},
onFailure: function(data) {
log("Unable to Load SwaggerUI");
},
docExpansion: "none"
});
$('#input_apiKey').change(function() {
var key = $('#input_apiKey')[0].value;
log("key: " + key);
if(key && key.trim() != "") {
log("added key " + key);
window.authorizations.add("key", new ApiKeyAuthorization("access_token", key, "query"));
}
})
window.swaggerUi.load();
});
</script>
</head>
<body class="swagger-section">
<div id="header">
<div class="swagger-ui-wrap">
<a id="logo" href="http://swagger.wordnik.com/">swagger</a>
<form id="api_selector">
<div class="input"><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"></div>
<div class="input"><input placeholder="access_token" id="input_apiKey" name="apiKey" type="text"></div>
</form>
</div>
</div>
<div id="message-bar" class="swagger-ui-wrap message-fail">Can't read from server. It may not have the appropriate access-control-origin settings.</div>
<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
</body></html>

View File

@@ -155,7 +155,7 @@ Protocol URLs
All these URLs are namespaced within a prefix of
/matrix/federation/v1/...
/_matrix/federation/v1/...
For active pushing of messages representing live activity "as it happens":

View File

@@ -35,8 +35,8 @@ 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.
The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user. They are
case-insensitive.
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
@@ -60,7 +60,8 @@ identified via a "Room ID", which look like::
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.
domain specified. Room IDs are not meant to be human readable. They ARE
case-sensitive.
The following diagram shows an ``m.room.message`` event being sent in the room
``!qporfwt:matrix.org``::
@@ -102,10 +103,10 @@ Each room can also have multiple "Room Aliases", which looks like::
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.
which can be used to publicise rooms. They are case-insensitive. 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.
::
@@ -132,49 +133,567 @@ Identity
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.
All communication in Matrix is performed over HTTP[S] using a Content-Type of ``application/json``.
Any errors which occur on the Matrix API level MUST return a "standard error response". This is a
JSON object which looks like::
{
"errcode": "<error code>",
"error": "<error message>"
}
The ``error`` string will be a human-readable error message, usually a sentence
explaining what went wrong. The ``errcode`` string will be a unique string which can be
used to handle an error message e.g. ``M_FORBIDDEN``. These error codes should have their
namespace first in ALL CAPS, followed by a single _. For example, if there was a custom
namespace ``com.mydomain.here``, and a ``FORBIDDEN`` code, the error code should look
like ``COM.MYDOMAIN.HERE_FORBIDDEN``. There may be additional keys depending on
the error, but the keys ``error`` and ``errcode`` MUST always be present.
Some standard error codes are below:
:``M_FORBIDDEN``:
Forbidden access, e.g. joining a room without permission, failed login.
:``M_UNKNOWN_TOKEN``:
The access token specified was not recognised.
:``M_BAD_JSON``:
Request contained valid JSON, but it was malformed in some way, e.g. missing
required keys, invalid values for keys.
:``M_NOT_JSON``:
Request did not contain valid JSON.
:``M_NOT_FOUND``:
No resource was found for this request.
:``M_LIMIT_EXCEEDED``:
Too many requests have been sent in a short period of time. Wait a while then
try again.
Some requests have unique error codes:
:``M_USER_IN_USE``:
Encountered when trying to register a user ID which has been taken.
:``M_ROOM_IN_USE``:
Encountered when trying to create a room which has been taken.
:``M_BAD_PAGINATION``:
Encountered when specifying bad pagination query parameters.
:``M_LOGIN_EMAIL_URL_NOT_YET``:
Encountered when polling for an email link which has not been clicked yet.
The C-S API typically uses ``HTTP POST`` to submit requests. This means these requests
are not idempotent. The C-S API also allows ``HTTP PUT`` to make requests idempotent.
In order to use a ``PUT``, paths should be suffixed with ``/{txnId}``. ``{txnId}`` is a
client-generated transaction ID which identifies the request. Crucially, it **only**
serves to identify new requests from retransmits. After the request has finished, the
``{txnId}`` value should be changed (how is not specified, it could be a monotonically
increasing integer, etc). It is preferable to use ``HTTP PUT`` to make sure requests to
send messages do not get sent more than once should clients need to retransmit requests.
Valid requests look like::
POST /some/path/here
{
"key": "This is a post."
}
PUT /some/path/here/11
{
"key": "This is a put with a txnId of 11."
}
In contrast, these are invalid requests::
POST /some/path/here/11
{
"key": "This is a post, but it has a txnId."
}
PUT /some/path/here
{
"key": "This is a put but it is missing a txnId."
}
- TODO: All strings everywhere are UTF-8
Receiving live updates on a client
----------------------------------
- C-S longpoll event stream
- Concept of start/end tokens.
- Mention /initialSync to get token.
Clients can receive new events by long-polling the home server. This will hold open the
HTTP connection for a short period of time waiting for new events, returning early if an
event occurs. This is called the "Event Stream". All events which the client is authorised
to view will appear in the event stream. When the stream is closed, an ``end`` token is
returned. This token can be used in the next request to continue where the client left off.
When the client first logs in, they will need to initially synchronise with their home
server. This is achieved via the ``/initialSync`` API. This API also returns an ``end``
token which can be used with the event stream.
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.
Creation
--------
To create a room, a client has to use the ``/createRoom`` API. There are various options
which can be set when creating a room:
Messages
========
``visibility``
Type:
String
Optional:
Yes
Value:
Either ``public`` or ``private``.
Description:
A ``public`` visibility indicates that the room will be shown in the public room list. A
``private`` visibility will hide the room from the public room list. Rooms default to
``public`` visibility if this key is not included.
``room_alias_name``
Type:
String
Optional:
Yes
Value:
The room alias localpart.
Description:
If this is included, a room alias will be created and mapped to the newly created room.
The alias will belong on the same home server which created the room, e.g.
``!qadnasoi:domain.com >>> #room_alias_name:domain.com``
``name``
Type:
String
Optional:
Yes
Value:
The ``name`` value for the ``m.room.name`` state event.
Description:
If this is included, an ``m.room.name`` event will be sent into the room to indicate the
name of the room. See "Room Events" for more information on ``m.room.name``.
``topic``
Type:
String
Optional:
Yes
Value:
The ``topic`` value for the ``m.room.topic`` state event.
Description:
If this is included, an ``m.room.topic`` event will be sent into the room to indicate the
topic for the room. See "Room Events" for more information on ``m.room.topic``.
Example::
{
"visibility": "public",
"room_alias_name": "the pub",
"name": "The Grand Duke Pub",
"topic": "All about happy hour"
}
- TODO: This creates a room creation event which serves as the root of the PDU graph for this room.
- TODO: Keys for speccing a room name / room topic / invite these users?
Modifying aliases
-----------------
- path to edit aliases
- format when retrieving list of aliases. NOT complete list.
- format for adding aliases.
Permissions
-----------
- TODO: What is a power level? How do they work? Defaults / required levels for X. How do they change
as people join and leave rooms? What do you do if you get a clash? Examples.
- TODO: List all actions which use power levels (sending msgs, inviting users, banning people, etc...)
- TODO: Room config - what is the event and what are the keys/values and explanations for them.
Link through to respective sections where necessary. How does this tie in with permissions, e.g.
give example of creating a read-only room.
Joining rooms
-------------
- TODO: What does the home server have to do to join a user to a room?
Users need to join a room in order to send and receive events in that room. A user can join a
room by making a request to ``/join/<room alias or id>`` with::
{}
Alternatively, a user can make a request to ``/rooms/<room id>/join`` with the same request content.
This is only provided for symmetry with the other membership APIs: ``/rooms/<room id>/invite`` and
``/rooms/<room id>/leave``. If a room alias was specified, it will be automatically resolved to
a room ID, which will then be joined. The room ID that was joined will be returned in response::
{
"room_id": "!roomid:domain"
}
The membership state for the joining user can also be modified directly to be ``join``
by sending the following request to
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
{
"membership": "join"
}
See the "Room events" section for more information on ``m.room.member``.
After the user has joined a room, they will receive subsequent events in that room. This room
will now appear as an entry in the ``/initialSync`` API.
Some rooms enforce that a user is *invited* to a room before they can join that room. Other
rooms will allow anyone to join the room even if they have not received an invite.
Inviting users
--------------
- Can invite users to a room if the room config key TODO is set to TODO. Must have required power level.
- Outline invite join dance. What is it? Why is it required? How does it work?
- What does the home server have to do?
The purpose of inviting users to a room is to notify them that the room exists
so they can choose to become a member of that room. Some rooms require that all
users who join a room are previously invited to it (an "invite-only" room).
Whether a given room is an "invite-only" room is determined by the room config
key ``TODO``. It can have one of the following values:
- TODO Room config invite only value explanation
- TODO Room config free-to-join value explanation
Only users who have a membership state of ``join`` in a room can invite new
users to said room. The person being invited must not be in the ``join`` state
in the room. The fully-qualified user ID must be specified when inviting a user,
as the user may reside on a different home server. To invite a user, send the
following request to ``/rooms/<room id>/invite``, which will manage the
entire invitation process::
{
"user_id": "<user id to invite>"
}
Alternatively, the membership state for this user in this room can be modified
directly by sending the following request to
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
{
"membership": "invite"
}
See the "Room events" section for more information on ``m.room.member``.
- TODO: In what circumstances will this NOT be equivalent to ``/invite``?
Leaving rooms
-------------
A user can leave a room to stop receiving events for that room. A user must have
joined the room before they are eligible to leave the room. If the room is an
"invite-only" room, they will need to be re-invited before they can re-join the room.
To leave a room, a request should be made to ``/rooms/<room id>/leave`` with::
{}
Alternatively, the membership state for this user in this room can be modified
directly by sending the following request to
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
{
"membership": "leave"
}
See the "Room events" section for more information on ``m.room.member``.
Once a user has left a room, that room will no longer appear on the ``/initialSync``
API. Be aware that leaving a room is not equivalent to have never been
in that room. A user who has previously left a room still maintains some residual state in
that room. Their membership state will be marked as ``leave``. This contrasts with
a user who has *never been invited or joined to that room* who will not have any
membership state for that room.
If all members in a room leave, that room becomes eligible for deletion.
- TODO: Grace period before deletion?
- TODO: Under what conditions should a room NOT be purged?
Banning users in a room
-----------------------
A user may decide to ban another user in a room. 'Banning' forces the target user
to leave the room and prevents them from re-joining the room. A banned user will
not be treated as a joined user, and so will not be able to send or receive events
in the room. In order to ban someone, the user performing the ban MUST have the
required power level. To ban a user, a request should be made to
``/rooms/<room id>/ban`` with::
{
"user_id": "<user id to ban"
"reason": "string: <reason for the ban>"
}
Banning a user adjusts the banned member's membership state to ``ban`` and adjusts
the power level of this event to a level higher than the banned person. Like
with other membership changes, a user can directly adjust the target member's
state, by making a request to ``/rooms/<room id>/state/m.room.member/<user id>``::
{
"membership": "ban"
}
Events in a room
----------------
Room events can be split into two categories:
:State Events:
These are events which replace events that came before it, depending on a set of unique keys.
These keys are the event ``type`` and a ``state_key``. Events with the same set of keys will
be overwritten. Typically, state events are used to store state, hence their name.
:Non-state events:
These are events which cannot be overwritten after sending. The list of events continues
to grow as more events are sent. As this list grows, it becomes necessary to
provide a mechanism for navigating this list. Pagination APIs are used to view the list
of historical non-state events. Typically, non-state events are used to send messages.
This specification outlines several events, all with the event type prefix ``m.``. However,
applications may wish to add their own type of event, and this can be achieved using the
REST API detailed in the following sections. If new events are added, the event ``type``
key SHOULD follow the Java package naming convention, e.g. ``com.example.myapp.event``.
This ensures event types are suitably namespaced for each application and reduces the
risk of clashes.
State events
------------
State events can be sent by ``PUT`` ing to ``/rooms/<room id>/state/<event type>/<state key>``.
These events will be overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all match.
If the state event has no ``state_key``, it can be omitted from the path. These requests
**cannot use transaction IDs** like other ``PUT`` paths because they cannot be differentiated
from the ``state_key``. Furthermore, ``POST`` is unsupported on state paths. Valid requests
look like::
PUT /rooms/!roomid:domain/state/m.example.event
{ "key" : "without a state key" }
PUT /rooms/!roomid:domain/state/m.another.example.event/foo
{ "key" : "with 'foo' as the state key" }
In contrast, these requests are invalid::
POST /rooms/!roomid:domain/state/m.example.event/
{ "key" : "cannot use POST here" }
PUT /rooms/!roomid:domain/state/m.another.example.event/foo/11
{ "key" : "txnIds are not supported" }
Care should be taken to avoid setting the wrong ``state key``::
PUT /rooms/!roomid:domain/state/m.another.example.event/11
{ "key" : "with '11' as the state key, but was probably intended to be a txnId" }
The ``state_key`` is often used to store state about individual users, by using the user ID as the
``state_key`` value. For example::
PUT /rooms/!roomid:domain/state/m.favorite.animal.event/%40my_user%3Adomain.com
{ "animal" : "cat", "reason": "fluffy" }
In some cases, there may be no need for a ``state_key``, so it can be omitted::
PUT /rooms/!roomid:domain/state/m.room.bgd.color
{ "color": "red", "hex": "#ff0000" }
See "Room Events" for the ``m.`` event specification.
Non-state events
----------------
Non-state events can be sent by sending a request to ``/rooms/<room id>/send/<event type>``.
These requests *can* use transaction IDs and ``PUT``/``POST`` methods. Non-state events
allow access to historical events and pagination, making it best suited for sending messages.
For example::
POST /rooms/!roomid:domain/send/m.custom.example.message
{ "text": "Hello world!" }
PUT /rooms/!roomid:domain/send/m.custom.example.message/11
{ "text": "Goodbye world!" }
See "Room Events" for the ``m.`` event specification.
Syncing rooms
-------------
When a client logs in, they may have a list of rooms which they have already joined. These rooms
may also have a list of events associated with them. The purpose of 'syncing' is to present the
current room and event information in a convenient, compact manner. The events returned are not
limited to room events; presence events will also be returned. There are two APIs provided:
- ``/initialSync`` : A global sync which will present room and event information for all rooms
the user has joined.
- ``/rooms/<room id>/initialSync`` : A sync scoped to a single room. Presents room and event
information for this room only.
- TODO: JSON response format for both types
- TODO: when would you use global? when would you use scoped?
Getting events for a room
-------------------------
There are several APIs provided to ``GET`` events for a room:
``/rooms/<room id>/state/<event type>/<state key>``
Description:
Get the state event identified.
Response format:
A JSON object representing the state event **content**.
Example:
``/rooms/!room:domain.com/state/m.room.name`` returns ``{ "name": "Room name" }``
``/rooms/<room id>/state``
Description:
Get all state events for a room.
Response format:
``[ { state event }, { state event }, ... ]``
Example:
TODO
``/rooms/<room id>/members``
Description:
Get all ``m.room.member`` state events.
Response format:
``{ "start": "token", "end": "token", "chunk": [ { m.room.member event }, ... ] }``
Example:
TODO
``/rooms/<room id>/messages``
Description:
Get all ``m.room.message`` events.
Response format:
``{ TODO }``
Example:
TODO
``/rooms/<room id>/initialSync``
Description:
Get all relevant events for a room. This includes state events, paginated non-state
events and presence events.
Response format:
`` { TODO } ``
Example:
TODO
Room Events
===========
- voip events?
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
``m.room.name``
Summary:
Set the human-readable name for the room.
Type:
State event
JSON format:
``{ "name" : "string" }``
Example:
``{ "name" : "My Room" }``
Description:
A room has an opaque room ID which is not human-friendly to read. A room alias is
human-friendly, but not all rooms have room aliases. The room name is a human-friendly
string designed to be displayed to the end-user. The room name is not *unique*, as
multiple rooms can have the same room name set. The room name can also be set when
creating a room using ``/createRoom`` with the ``name`` key.
What are they, when are they used, what do they contain, how should they be used
``m.room.topic``
Summary:
Set a topic for the room.
Type:
State event
JSON format:
``{ "topic" : "string" }``
Example:
``{ "topic" : "Welcome to the real world." }``
Description:
A topic is a short message detailing what is currently being discussed in the room.
It can also be used as a way to display extra information about the room, which may
not be suitable for the room name.
Non-state messages
------------------
- m.room.message
- m.room.message.feedback (and compressed format)
``m.room.member``
Summary:
The current membership state of a user in the room.
Type:
State event
JSON format:
``{ "membership" : "enum[ invite|join|leave|ban ]" }``
Example:
``{ "membership" : "join" }``
Description:
Adjusts the membership state for a user in a room. It is preferable to use the
membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions
rather than adjusting the state directly as there are a restricted set of valid
transformations. For example, user A cannot force user B to join a room, and trying
to force this state change directly will fail. See the "Rooms" section for how to
use the membership APIs.
What are they, when are they used, what do they contain, how should they be used
``m.room.config``
Summary:
The room config.
Type:
State event
JSON format:
TODO
Example:
TODO
Description:
TODO
``m.room.invite_join``
Summary:
TODO.
Type:
State event
JSON format:
TODO
Example:
TODO
Description:
TODO
``m.room.message``
Summary:
A message.
Type:
Non-state event
JSON format:
``{ "msgtype": "string" }``
Example:
``{ "msgtype": "m.text", "body": "Testing" }``
Description:
This event is used when sending messages in a room. Messages are not limited to be text.
The ``msgtype`` key outlines the type of message, e.g. text, audio, image, video, etc.
Whilst not required, the ``body`` key SHOULD be used with every kind of ``msgtype`` as
a fallback mechanism when a client cannot render the message. For more information on
the types of messages which can be sent, see "m.room.message msgtypes".
``m.room.message.feedback``
Summary:
A receipt for a message.
Type:
Non-state event
JSON format:
``{ "type": "enum [ delivered|read ]", "target_event_id": "string" }``
Example:
``{ "type": "delivered", "target_event_id": "e3b2icys" }``
Description:
Feedback events are events sent to acknowledge a message in some way. There are two
supported acknowledgements: ``delivered`` (sent when the event has been received) and
``read`` (sent when the event has been observed by the end-user). The ``target_event_id``
should reference the ``m.room.message`` event being acknowledged.
m.room.message msgtypes
-----------------------
@@ -285,7 +804,7 @@ 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:
is represented by the ``presence`` 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.
@@ -295,18 +814,26 @@ is represented by the ``state`` key, which is an enum of one of the following:
- ``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
This basic ``presence`` 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.
In addition, the server maintains a timestamp of the last time it saw an active
action from the user; either sending a message to a room, or changing presence
state from a lower to a higher level of availability (thus: changing state from
``unavailable`` to ``online`` will count as an action for being active, whereas
in the other direction will not). This timestamp is presented via a key called
``last_active_ago``, which gives the relative number of miliseconds since the
message is generated/emitted, that the user was last seen active.
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.
As well as the basic ``presence`` 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
------------
@@ -336,11 +863,23 @@ presence information in a user list for a room.
Typing notifications
====================
- what is the event type. Are they bundled with other event types? If so, which.
- what are the valid keys / values. What do they represent. Any gotchas?
- Timeouts. How do they work, who sets them and how do they expire. Does one
have priority over another? Give examples.
TODO : Leo
Voice over IP
=============
- what are the event types.
- what are the valid keys/values. What do they represent. Any gotchas?
- In what sequence should the events be sent?
- How do you accept / decline inbound calls? How do you make outbound calls?
Give examples.
- How does negotiation work? Give examples.
- How do you hang up?
- What does call log information look like e.g. duration of call?
TODO : Dave
@@ -358,8 +897,12 @@ 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?
- Changing profile info generates m.presence events ("presencelike")
- keys on m.presence are optional, except presence which is required
- m.room.member is populated with the current displayname at that point in time.
- That is added by the HS, not you.
- Display name changes also generates m.room.member with displayname key f.e. room
the user is in.
Registration and login
======================
@@ -823,11 +1366,16 @@ TODO
Content repository
==================
- thumbnail paths
- path to upload
- format for thumbnail paths, mention what it is protecting against.
- content size limit and associated M_ERROR.
Address book repository
=======================
- format
- format: POST(?) wodges of json, some possible processing, then return wodges of json on GET.
- processing may remove dupes, merge contacts, pepper with extra info (e.g. matrix-ability of
contacts), etc.
- Standard json format for contacts? Piggy back off vcards?
Glossary

View File

@@ -120,7 +120,7 @@ def make_graph(pdus, room, filename_prefix):
def get_pdus(host, room):
transaction = json.loads(
urllib2.urlopen(
"http://%s/matrix/federation/v1/context/%s/" % (host, room)
"http://%s/_matrix/federation/v1/context/%s/" % (host, room)
).read()
)

View File

@@ -10,7 +10,7 @@ $('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@@ -25,11 +25,12 @@ $('.login').live('click', function() {
});
var getCurrentRoomList = function() {
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
for (var i=0; i<data.length; ++i) {
data[i].latest_message = data[i].messages.chunk[0].content.body;
addRoom(data[i]);
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
addRoom(rooms[i]);
}
}).fail(function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
@@ -43,7 +44,7 @@ $('.createRoom').live('click', function() {
data.room_alias_name = roomAlias;
}
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
@@ -78,11 +79,9 @@ $('.sendMessage').live('click', function() {
return;
}
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
url = url.replace("$msgid", msgId);
var data = {
msgtype: "m.text",
@@ -91,7 +90,7 @@ $('.sendMessage').live('click', function() {
$.ajax({
url: url,
type: "PUT",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",

View File

@@ -7,7 +7,7 @@ var eventStreamInfo = {
var roomInfo = [];
var longpollEventStream = function() {
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$from", eventStreamInfo.from);
@@ -48,7 +48,7 @@ $('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@@ -65,14 +65,15 @@ $('.login').live('click', function() {
var getCurrentRoomList = function() {
$("#roomId").val("");
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
for (var i=0; i<data.length; ++i) {
if ("messages" in data[i]) {
data[i].latest_message = data[i].messages.chunk[0].content.body;
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
if ("messages" in rooms[i]) {
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
}
}
roomInfo = data;
roomInfo = rooms;
setRooms(roomInfo);
}).fail(function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
@@ -92,17 +93,14 @@ $('.sendMessage').live('click', function() {
var sendMessage = function(roomId) {
var body = "jsfiddle message @" + $.now();
var msgId = $.now();
if (roomId.length === 0) {
return;
}
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
url = url.replace("$msgid", msgId);
var data = {
msgtype: "m.text",
@@ -111,7 +109,7 @@ var sendMessage = function(roomId) {
$.ajax({
url: url,
type: "PUT",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",

View File

@@ -10,7 +10,7 @@ var viewingRoomId;
// ************** Event Streaming **************
var longpollEventStream = function() {
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$from", eventStreamInfo.from);
@@ -38,8 +38,9 @@ var longpollEventStream = function() {
else if (data.chunk[i].type === "m.room.member") {
if (viewingRoomId === data.chunk[i].room_id) {
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
addMessage(data.chunk[i]);
for (j=0; j<memberInfo.length; ++j) {
if (memberInfo[j].target_user_id === data.chunk[i].target_user_id) {
if (memberInfo[j].state_key === data.chunk[i].state_key) {
memberInfo[j] = data.chunk[i];
updatedMemberList = true;
break;
@@ -50,7 +51,7 @@ var longpollEventStream = function() {
updatedMemberList = true;
}
}
if (data.chunk[i].target_user_id === accountInfo.user_id) {
if (data.chunk[i].state_key === accountInfo.user_id) {
getCurrentRoomList(); // update our join/invite list
}
}
@@ -88,7 +89,7 @@ $('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@@ -106,7 +107,7 @@ $('.register').live('click', function() {
var user = $("#userReg").val();
var password = $("#passwordReg").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/register",
url: "http://localhost:8080/_matrix/client/api/v1/register",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user_id: user, password: password }),
@@ -133,7 +134,7 @@ $('.createRoom').live('click', function() {
data.room_alias_name = roomAlias;
}
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
@@ -154,14 +155,15 @@ $('.createRoom').live('click', function() {
// ************** Getting current state **************
var getCurrentRoomList = function() {
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
for (var i=0; i<data.length; ++i) {
if ("messages" in data[i]) {
data[i].latest_message = data[i].messages.chunk[0].content.body;
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
if ("messages" in rooms[i]) {
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
}
}
roomInfo = data;
roomInfo = rooms;
setRooms(roomInfo);
}).fail(function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
@@ -179,7 +181,8 @@ var loadRoomContent = function(roomId) {
var getMessages = function(roomId) {
$("#messages").empty();
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/messages/list?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=10";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +
encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
$.getJSON(url, function(data) {
for (var i=data.chunk.length-1; i>=0; --i) {
addMessage(data.chunk[i]);
@@ -190,7 +193,8 @@ var getMessages = function(roomId) {
var getMemberList = function(roomId) {
$("#members").empty();
memberInfo = [];
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/members/list?access_token=" + accountInfo.access_token;
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +
encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
$.getJSON(url, function(data) {
for (var i=0; i<data.chunk.length; ++i) {
memberInfo.push(data.chunk[i]);
@@ -212,11 +216,9 @@ $('.sendMessage').live('click', function() {
var sendMessage = function(roomId, body) {
var msgId = $.now();
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
url = url.replace("$msgid", msgId);
var data = {
msgtype: "m.text",
@@ -225,7 +227,7 @@ var sendMessage = function(roomId, body) {
$.ajax({
url: url,
type: "PUT",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
@@ -260,13 +262,12 @@ var setRooms = function(roomList) {
var membership = $(this).find('td:eq(1)').text();
if (membership !== "join") {
console.log("Joining room " + roomId);
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
$.ajax({
url: url,
type: "PUT",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({membership: "join"}),
dataType: "json",
@@ -286,16 +287,33 @@ var setRooms = function(roomList) {
};
var addMessage = function(data) {
var msg = data.content.body;
if (data.type === "m.room.member") {
if (data.content.membership === "invite") {
msg = "<em>invited " + data.state_key + " to the room</em>";
}
else if (data.content.membership === "join") {
msg = "<em>joined the room</em>";
}
else if (data.content.membership === "leave") {
msg = "<em>left the room</em>";
}
else {
msg = "<em>" + data.content.membership + "</em>";
}
}
var row = "<tr>" +
"<td>"+data.user_id+"</td>" +
"<td>"+data.content.body+"</td>" +
"<td>"+msg+"</td>" +
"</tr>";
$("#messages").append(row);
};
var addMember = function(data) {
var row = "<tr>" +
"<td>"+data.target_user_id+"</td>" +
"<td>"+data.state_key+"</td>" +
"<td>"+data.content.membership+"</td>" +
"</tr>";
$("#members").append(row);

View File

@@ -11,7 +11,7 @@ $('.register').live('click', function() {
var user = $("#user").val();
var password = $("#password").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/register",
url: "http://localhost:8080/_matrix/client/api/v1/register",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user_id: user, password: password }),
@@ -27,7 +27,7 @@ $('.register').live('click', function() {
var login = function(user, password) {
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@@ -44,8 +44,8 @@ var login = function(user, password) {
$('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.getJSON("http://localhost:8080/matrix/client/api/v1/login", function(data) {
if (data.type !== "m.login.password") {
$.getJSON("http://localhost:8080/_matrix/client/api/v1/login", function(data) {
if (data.flows[0].type !== "m.login.password") {
alert("I don't know how to login with this type: " + data.type);
return;
}
@@ -60,7 +60,7 @@ $('.logout').live('click', function() {
});
$('.testToken').live('click', function() {
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
}).fail(function(err) {

View File

@@ -14,9 +14,9 @@
<input type="text" id="roomId" placeholder="Room ID"></input>
<input type="text" id="targetUser" placeholder="Target User ID"></input>
<select id="membership">
<option value="invite">Invite</option>
<option value="join">Join</option>
<option value="leave">Leave</option>
<option value="invite">invite</option>
<option value="join">join</option>
<option value="leave">leave</option>
</select>
<input type="button" class="changeMembership" value="Change Membership"></input>
</form>

View File

@@ -4,13 +4,21 @@ var showLoggedIn = function(data) {
accountInfo = data;
getCurrentRoomList();
$(".loggedin").css({visibility: "visible"});
$("#membership").change(function() {
if ($("#membership").val() === "invite") {
$("#targetUser").css({visibility: "visible"});
}
else {
$("#targetUser").css({visibility: "hidden"});
}
});
};
$('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/login",
url: "http://localhost:8080/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
@@ -31,10 +39,11 @@ var getCurrentRoomList = function() {
// solution but that is out of scope of this fiddle.
$("#rooms").find("tr:gt(0)").remove();
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
for (var i=0; i<data.length; ++i) {
addRoom(data[i]);
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
addRoom(rooms[i]);
}
}).fail(function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
@@ -44,7 +53,7 @@ var getCurrentRoomList = function() {
$('.createRoom').live('click', function() {
var data = {};
$.ajax({
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
@@ -78,54 +87,42 @@ $('.changeMembership').live('click', function() {
return;
}
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
url = url.replace("$user", encodeURIComponent(member));
url = url.replace("$membership", membership);
if (membership === "leave") {
$.ajax({
url: url,
type: "DELETE",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(data) {
getCurrentRoomList();
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
}
else {
var data = {
membership: membership
var data = {};
if (membership === "invite") {
data = {
user_id: member
};
$.ajax({
url: url,
type: "PUT",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(data) {
getCurrentRoomList();
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
}
$.ajax({
url: url,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(data) {
getCurrentRoomList();
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
});
$('.joinAlias').live('click', function() {
var roomAlias = $("#roomAlias").val();
var url = "http://localhost:8080/matrix/client/api/v1/join/$roomalias?access_token=$token";
var url = "http://localhost:8080/_matrix/client/api/v1/join/$roomalias?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
$.ajax({
url: url,
type: "PUT",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({}),
dataType: "json",

View File

@@ -34,6 +34,7 @@ setup(
"syutil==0.0.1",
"Twisted>=14.0.0",
"service_identity>=1.0.0",
"pyyaml",
"pyasn1",
"pynacl",
"daemonize",

View File

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

View File

@@ -17,9 +17,10 @@
from twisted.internet import defer
from synapse.api.constants import Membership
from synapse.api.constants import Membership, JoinRules
from synapse.api.errors import AuthError, StoreError, Codes
from synapse.api.events.room import RoomMemberEvent
from synapse.util.logutils import log_function
import logging
@@ -44,16 +45,29 @@ class Auth(object):
"""
try:
if hasattr(event, "room_id"):
is_state = hasattr(event, "state_key")
if event.type == RoomMemberEvent.TYPE:
yield self._can_replace_state(event)
allowed = yield self.is_membership_change_allowed(event)
defer.returnValue(allowed)
return
self._check_joined_room(
member=snapshot.membership_state,
user_id=snapshot.user_id,
room_id=snapshot.room_id,
)
if is_state:
# TODO (erikj): This really only should be called for *new*
# state
yield self._can_add_state(event)
yield self._can_replace_state(event)
else:
self._check_joined_room(
member=snapshot.membership_state,
user_id=snapshot.user_id,
room_id=snapshot.room_id,
)
defer.returnValue(True)
yield self._can_send_event(event)
defer.returnValue(True)
else:
raise AuthError(500, "Unknown event: %s" % event)
except AuthError as e:
@@ -111,7 +125,14 @@ class Auth(object):
membership = event.content["membership"]
join_rule = yield self.store.get_room_join_rule(event.room_id)
if not join_rule:
join_rule = JoinRules.INVITE
if Membership.INVITE == membership:
# TODO (erikj): We should probably handle this more intelligently
# PRIVATE join rules.
# Invites are valid iff caller is in the room and target isn't.
if not caller_in_room: # caller isn't joined
raise AuthError(403, "You are not in room %s." % event.room_id)
@@ -124,18 +145,54 @@ class Auth(object):
# joined: It's a NOOP
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.
elif (not caller or caller.membership not in
[Membership.INVITE, Membership.JOIN]):
raise AuthError(403, "You are not invited to this room.")
elif join_rule == JoinRules.PUBLIC or room.is_public:
pass
elif join_rule == JoinRules.INVITE:
if (
not caller or caller.membership not in
[Membership.INVITE, Membership.JOIN]
):
raise AuthError(403, "You are not invited to this room.")
else:
# TODO (erikj): may_join list
# TODO (erikj): private rooms
raise AuthError(403, "You are not allowed to join this room")
elif Membership.LEAVE == membership:
# TODO (erikj): Implement kicks.
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 target_user_id != event.user_id:
# trying to force another user to leave
raise AuthError(403, "Cannot force %s to leave." %
target_user_id)
user_level = yield self.store.get_power_level(
event.room_id,
event.user_id,
)
_, kick_level = yield self.store.get_ops_levels(event.room_id)
if kick_level:
kick_level = int(kick_level)
else:
kick_level = 5
if user_level < kick_level:
raise AuthError(
403, "You cannot kick user %s." % target_user_id
)
elif Membership.BAN == membership:
user_level = yield self.store.get_power_level(
event.room_id,
event.user_id,
)
ban_level, _ = yield self.store.get_ops_levels(event.room_id)
if ban_level:
ban_level = int(ban_level)
else:
ban_level = 5 # FIXME (erikj): What should we do here?
if user_level < ban_level:
raise AuthError(403, "You don't have permission to ban")
else:
raise AuthError(500, "Unknown membership %s" % membership)
@@ -176,3 +233,85 @@ class Auth(object):
except StoreError:
raise AuthError(403, "Unrecognised access token.",
errcode=Codes.UNKNOWN_TOKEN)
@defer.inlineCallbacks
@log_function
def _can_send_event(self, event):
send_level = yield self.store.get_send_event_level(event.room_id)
if send_level:
send_level = int(send_level)
else:
send_level = 0
user_level = yield self.store.get_power_level(
event.room_id,
event.user_id,
)
if user_level:
user_level = int(user_level)
else:
user_level = 0
if user_level < send_level:
raise AuthError(
403, "You don't have permission to post to the room"
)
defer.returnValue(True)
@defer.inlineCallbacks
def _can_add_state(self, event):
add_level = yield self.store.get_add_state_level(event.room_id)
if not add_level:
defer.returnValue(True)
add_level = int(add_level)
user_level = yield self.store.get_power_level(
event.room_id,
event.user_id,
)
user_level = int(user_level)
if user_level < add_level:
raise AuthError(
403, "You don't have permission to add state to the room"
)
defer.returnValue(True)
@defer.inlineCallbacks
def _can_replace_state(self, event):
current_state = yield self.store.get_current_state(
event.room_id,
event.type,
event.state_key,
)
if current_state:
current_state = current_state[0]
user_level = yield self.store.get_power_level(
event.room_id,
event.user_id,
)
if user_level:
user_level = int(user_level)
else:
user_level = 0
logger.debug("Checking power level for %s, %s", event.user_id, user_level)
if current_state and hasattr(current_state, "required_power_level"):
req = current_state.required_power_level
logger.debug("Checked power level for %s, %s", event.user_id, req)
if user_level < req:
raise AuthError(
403,
"You don't have permission to change that state"
)

View File

@@ -23,7 +23,8 @@ class Membership(object):
JOIN = u"join"
KNOCK = u"knock"
LEAVE = u"leave"
LIST = (INVITE, JOIN, KNOCK, LEAVE)
BAN = u"ban"
LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN)
class Feedback(object):
@@ -42,3 +43,10 @@ class PresenceState(object):
UNAVAILABLE = u"unavailable"
ONLINE = u"online"
FREE_FOR_CHAT = u"free_for_chat"
class JoinRules(object):
PUBLIC = u"public"
KNOCK = u"knock"
INVITE = u"invite"
PRIVATE = u"private"

View File

@@ -42,6 +42,7 @@ class SynapseEvent(JsonEncodedObject):
"user_id", # sender/initiator
"content", # HTTP body, JSON
"state_key",
"required_power_level",
]
internal_keys = [
@@ -52,6 +53,7 @@ class SynapseEvent(JsonEncodedObject):
"destinations",
"origin",
"outlier",
"power_level",
]
required_keys = [
@@ -152,3 +154,10 @@ class SynapseEvent(JsonEncodedObject):
msg = self._check_json(entry, template[key][0])
if msg:
return msg
class SynapseStateEvent(SynapseEvent):
def __init__(self, **kwargs):
if "state_key" not in kwargs:
kwargs["state_key"] = ""
super(SynapseStateEvent, self).__init__(**kwargs)

View File

@@ -16,6 +16,8 @@
from synapse.api.events.room import (
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent
)
from synapse.util.stringutils import random_string
@@ -30,7 +32,13 @@ class EventFactory(object):
RoomMemberEvent,
FeedbackEvent,
InviteJoinEvent,
RoomConfigEvent
RoomConfigEvent,
RoomPowerLevelsEvent,
RoomJoinRulesEvent,
RoomCreateEvent,
RoomAddStateLevelEvent,
RoomSendEventLevelEvent,
RoomOpsPowerLevelsEvent,
]
def __init__(self, hs):

View File

@@ -15,7 +15,7 @@
from synapse.api.constants import Feedback, Membership
from synapse.api.errors import SynapseError
from . import SynapseEvent
from . import SynapseEvent, SynapseStateEvent
class GenericEvent(SynapseEvent):
@@ -132,3 +132,45 @@ class RoomConfigEvent(SynapseEvent):
def get_content_template(self):
return {}
class RoomCreateEvent(SynapseStateEvent):
TYPE = "m.room.create"
def get_content_template(self):
return {}
class RoomJoinRulesEvent(SynapseStateEvent):
TYPE = "m.room.join_rules"
def get_content_template(self):
return {}
class RoomPowerLevelsEvent(SynapseStateEvent):
TYPE = "m.room.power_levels"
def get_content_template(self):
return {}
class RoomAddStateLevelEvent(SynapseStateEvent):
TYPE = "m.room.add_state_level"
def get_content_template(self):
return {}
class RoomSendEventLevelEvent(SynapseStateEvent):
TYPE = "m.room.send_event_level"
def get_content_template(self):
return {}
class RoomOpsPowerLevelsEvent(SynapseStateEvent):
TYPE = "m.room.ops_levels"
def get_content_template(self):
return {}

View File

@@ -15,7 +15,7 @@
"""Contains the URL paths to prefix various aspects of the server with. """
CLIENT_PREFIX = "/matrix/client/api/v1"
FEDERATION_PREFIX = "/matrix/federation/v1"
WEB_CLIENT_PREFIX = "/matrix/client"
CONTENT_REPO_PREFIX = "/matrix/content"
CLIENT_PREFIX = "/_matrix/client/api/v1"
FEDERATION_PREFIX = "/_matrix/federation/v1"
WEB_CLIENT_PREFIX = "/_matrix/client"
CONTENT_REPO_PREFIX = "/_matrix/content"

View File

@@ -20,7 +20,6 @@ from synapse.server import HomeServer
from twisted.internet import reactor
from twisted.enterprise import adbapi
from twisted.python.log import PythonLoggingObserver
from twisted.web.resource import Resource
from twisted.web.static import File
from twisted.web.server import Site
@@ -29,16 +28,17 @@ from synapse.http.client import TwistedHttpClient
from synapse.api.urls import (
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
)
from synapse.config.homeserver import HomeServerConfig
from synapse.crypto import context_factory
from daemonize import Daemonize
import twisted.manhole.telnet
import argparse
import logging
import logging.config
import sqlite3
import os
import re
import sys
logger = logging.getLogger(__name__)
@@ -56,13 +56,13 @@ SCHEMAS = [
# 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
SCHEMA_VERSION = 2
class SynapseHomeServer(HomeServer):
def build_http_client(self):
return TwistedHttpClient()
return TwistedHttpClient(self)
def build_resource_for_client(self):
return JsonResource()
@@ -206,37 +206,17 @@ class SynapseHomeServer(HomeServer):
"""
return "%s-%s" % (resource, path_seg)
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):
""" Sets up logging with verbosity levels.
Args:
verbosity: The verbosity level.
filename: Log to the given file rather than to the console.
config_path: Path to a python logging config file.
"""
if config_path is None:
log_format = (
'%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
)
level = logging.INFO
if verbosity:
level = logging.DEBUG
# FIXME: we need a logging.WARN for a -q quiet option
logging.basicConfig(level=level, filename=filename, format=log_format)
else:
logging.config.fileConfig(config_path)
observer = PythonLoggingObserver()
observer.start()
def start_listening(self, secure_port, unsecure_port):
if secure_port is not None:
reactor.listenSSL(
secure_port, Site(self.root_resource), self.tls_context_factory
)
logger.info("Synapse now listening on port %d", secure_port)
if unsecure_port is not None:
reactor.listenTCP(
unsecure_port, Site(self.root_resource)
)
logger.info("Synapse now listening on port %d", unsecure_port)
def run():
@@ -244,78 +224,53 @@ def run():
def setup():
parser = argparse.ArgumentParser()
parser.add_argument("-p", "--port", dest="port", type=int, default=8080,
help="The port to listen on.")
parser.add_argument("-d", "--database", dest="db", default="homeserver.db",
help="The database name.")
parser.add_argument("-H", "--host", dest="host", default="localhost",
help="The hostname of the server.")
parser.add_argument('-v', '--verbose', dest="verbose", action='count',
help="The verbosity level.")
parser.add_argument('-f', '--log-file', dest="log_file", default=None,
help="File to log to.")
parser.add_argument('--log-config', dest="log_config", default=None,
help="Python logging config")
parser.add_argument('-D', '--daemonize', action='store_true',
default=False, help="Daemonize the home server")
parser.add_argument('--pid-file', dest="pid", help="When running as a "
"daemon, the file to store the pid in",
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
# Because if/when we daemonize we change to root dir.
db_name = os.path.abspath(args.db)
log_file = args.log_file
if log_file:
log_file = os.path.abspath(log_file)
setup_logging(
verbosity=verbosity,
filename=log_file,
config_path=args.log_config,
config = HomeServerConfig.load_config(
"Synapse Homeserver",
sys.argv[1:],
generate_section="Homeserver"
)
logger.info("Server hostname: %s", args.host)
config.setup_logging()
if re.search(":[0-9]+$", args.host):
domain_with_port = args.host
logger.info("Server hostname: %s", config.server_name)
if re.search(":[0-9]+$", config.server_name):
domain_with_port = config.server_name
else:
domain_with_port = "%s:%s" % (args.host, args.port)
domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
tls_context_factory = context_factory.ServerContextFactory(config)
hs = SynapseHomeServer(
args.host,
config.server_name,
domain_with_port=domain_with_port,
upload_dir=os.path.abspath("uploads"),
db_name=db_name,
db_name=config.database_path,
tls_context_factory=tls_context_factory,
)
hs.register_servlets()
hs.create_resource_tree(
web_client=args.webclient,
redirect_root_to_web_client=True)
hs.start_listening(args.port)
web_client=config.webclient,
redirect_root_to_web_client=True,
)
hs.start_listening(config.bind_port, config.unsecure_port)
hs.get_db_pool()
if args.manhole:
if config.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')
reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
if args.daemonize:
if config.daemonize:
print config.pid_file
daemon = Daemonize(
app="synapse-homeserver",
pid=args.pid,
pid=config.pid_file,
action=run,
auto_close_fds=False,
verbose=True,

View File

@@ -0,0 +1,14 @@
# -*- 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.

130
synapse/config/_base.py Normal file
View File

@@ -0,0 +1,130 @@
# -*- 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.
import ConfigParser as configparser
import argparse
import sys
import os
import yaml
class ConfigError(Exception):
pass
class Config(object):
def __init__(self, args):
pass
@staticmethod
def abspath(file_path):
return os.path.abspath(file_path) if file_path else file_path
@classmethod
def check_file(cls, file_path, config_name):
if file_path is None:
raise ConfigError(
"Missing config for %s."
" Try running again with --generate-config"
% (config_name,)
)
if not os.path.exists(file_path):
raise ConfigError(
"File % config for %s doesn't exist."
" Try running again with --generate-config"
% (config_name,)
)
return cls.abspath(file_path)
@classmethod
def read_file(cls, file_path, config_name):
cls.check_file(file_path, config_name)
with open(file_path) as file_stream:
return file_stream.read()
@staticmethod
def read_config_file(file_path):
with open(file_path) as file_stream:
return yaml.load(file_stream)
@classmethod
def add_arguments(cls, parser):
pass
@classmethod
def generate_config(cls, args, config_dir_path):
pass
@classmethod
def load_config(cls, description, argv, generate_section=None):
config_parser = argparse.ArgumentParser(add_help=False)
config_parser.add_argument(
"-c", "--config-path",
metavar="CONFIG_FILE",
help="Specify config file"
)
config_parser.add_argument(
"--generate-config",
action="store_true",
help="Generate config file"
)
config_args, remaining_args = config_parser.parse_known_args(argv)
if config_args.generate_config:
if not config_args.config_path:
config_parser.error(
"Must specify where to generate the config file"
)
config_dir_path = os.path.dirname(config_args.config_path)
if os.path.exists(config_args.config_path):
defaults = cls.read_config_file(config_args.config_path)
else:
defaults = {}
else:
if config_args.config_path:
defaults = cls.read_config_file(config_args.config_path)
else:
defaults = {}
parser = argparse.ArgumentParser(
parents=[config_parser],
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
cls.add_arguments(parser)
parser.set_defaults(**defaults)
args = parser.parse_args(remaining_args)
if config_args.generate_config:
config_dir_path = os.path.dirname(config_args.config_path)
config_dir_path = os.path.abspath(config_dir_path)
if not os.path.exists(config_dir_path):
os.makedirs(config_dir_path)
cls.generate_config(args, config_dir_path)
config = {}
for key, value in vars(args).items():
if (key not in set(["config_path", "generate_config"])
and value is not None):
config[key] = value
with open(config_args.config_path, "w") as config_file:
yaml.dump(config, config_file, default_flow_style=False)
sys.exit(0)
return cls(args)

View File

@@ -0,0 +1,37 @@
# -*- 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 ._base import Config
import os
class DatabaseConfig(Config):
def __init__(self, args):
super(DatabaseConfig, self).__init__(args)
self.database_path = self.abspath(args.database_path)
@classmethod
def add_arguments(cls, parser):
super(DatabaseConfig, cls).add_arguments(parser)
db_group = parser.add_argument_group("database")
db_group.add_argument(
"-d", "--database-path", default="homeserver.db",
help="The database name."
)
@classmethod
def generate_config(cls, args, config_dir_path):
super(DatabaseConfig, cls).generate_config(args, config_dir_path)
args.database_path = os.path.abspath(args.database_path)

View File

@@ -0,0 +1,26 @@
# -*- 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 .tls import TlsConfig
from .server import ServerConfig
from .logger import LoggingConfig
from .database import DatabaseConfig
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig):
pass
if __name__=='__main__':
import sys
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")

67
synapse/config/logger.py Normal file
View File

@@ -0,0 +1,67 @@
# -*- 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 ._base import Config
from twisted.python.log import PythonLoggingObserver
import logging
import logging.config
class LoggingConfig(Config):
def __init__(self, args):
super(LoggingConfig, self).__init__(args)
self.verbosity = int(args.verbose) if args.verbose else None
self.log_config = self.abspath(args.log_config)
self.log_file = self.abspath(args.log_file)
@classmethod
def add_arguments(cls, parser):
super(LoggingConfig, cls).add_arguments(parser)
logging_group = parser.add_argument_group("logging")
logging_group.add_argument(
'-v', '--verbose', dest="verbose", action='count',
help="The verbosity level."
)
logging_group.add_argument(
'-f', '--log-file', dest="log_file", default=None,
help="File to log to."
)
logging_group.add_argument(
'--log-config', dest="log_config", default=None,
help="Python logging config file"
)
def setup_logging(self):
log_format = (
'%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
)
if self.log_config is None:
level = logging.INFO
if self.verbosity:
level = logging.DEBUG
# FIXME: we need a logging.WARN for a -q quiet option
logging.basicConfig(
level=level,
filename=self.log_file,
format=log_format
)
else:
logging.config.fileConfig(self.log_config)
observer = PythonLoggingObserver()
observer.start()

79
synapse/config/server.py Normal file
View File

@@ -0,0 +1,79 @@
# -*- 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.
import nacl.signing
import os
from ._base import Config
from syutil.base64util import encode_base64, decode_base64
class ServerConfig(Config):
def __init__(self, args):
super(ServerConfig, self).__init__(args)
self.server_name = args.server_name
self.signing_key = self.read_signing_key(args.signing_key_path)
self.bind_port = args.bind_port
self.bind_host = args.bind_host
self.unsecure_port = args.unsecure_port
self.daemonize = args.daemonize
self.pid_file = self.abspath(args.pid_file)
self.webclient = True
self.manhole = args.manhole
@classmethod
def add_arguments(cls, parser):
super(ServerConfig, cls).add_arguments(parser)
server_group = parser.add_argument_group("server")
server_group.add_argument("-H", "--server-name", default="localhost",
help="The name of the server")
server_group.add_argument("--signing-key-path",
help="The signing key to sign messages with")
server_group.add_argument("-p", "--bind-port", metavar="PORT",
type=int, help="https port to listen on",
default=8448)
server_group.add_argument("--unsecure-port", metavar="PORT",
type=int, help="http port to listen on",
default=8008)
server_group.add_argument("--bind-host", default="",
help="Local interface to listen on")
server_group.add_argument("-D", "--daemonize", action='store_true',
help="Daemonize the home server")
server_group.add_argument('--pid-file', default="hs.pid",
help="When running as a daemon, the file to"
" store the pid in")
server_group.add_argument("--manhole", metavar="PORT", dest="manhole",
type=int,
help="Turn on the twisted telnet manhole"
" service on the given port.")
def read_signing_key(self, signing_key_path):
signing_key_base64 = self.read_file(signing_key_path, "signing_key")
signing_key_bytes = decode_base64(signing_key_base64)
return nacl.signing.SigningKey(signing_key_bytes)
@classmethod
def generate_config(cls, args, config_dir_path):
super(ServerConfig, cls).generate_config(args, config_dir_path)
base_key_name = os.path.join(config_dir_path, args.server_name)
args.pid_file = os.path.abspath(args.pid_file)
if not args.signing_key_path:
args.signing_key_path = base_key_name + ".signing.key"
if not os.path.exists(args.signing_key_path):
with open(args.signing_key_path, "w") as signing_key_file:
key = nacl.signing.SigningKey.generate()
signing_key_file.write(encode_base64(key.encode()))

130
synapse/config/tls.py Normal file
View File

@@ -0,0 +1,130 @@
# -*- 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 ._base import Config
from OpenSSL import crypto
import subprocess
import os
GENERATE_DH_PARAMS=False
class TlsConfig(Config):
def __init__(self, args):
super(TlsConfig, self).__init__(args)
self.tls_certificate = self.read_tls_certificate(
args.tls_certificate_path
)
self.tls_private_key = self.read_tls_private_key(
args.tls_private_key_path
)
self.tls_dh_params_path = self.check_file(
args.tls_dh_params_path, "tls_dh_params"
)
@classmethod
def add_arguments(cls, parser):
super(TlsConfig, cls).add_arguments(parser)
tls_group = parser.add_argument_group("tls")
tls_group.add_argument("--tls-certificate-path",
help="PEM encoded X509 certificate for TLS")
tls_group.add_argument("--tls-private-key-path",
help="PEM encoded private key for TLS")
tls_group.add_argument("--tls-dh-params-path",
help="PEM dh parameters for ephemeral keys")
def read_tls_certificate(self, cert_path):
cert_pem = self.read_file(cert_path, "tls_certificate")
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
def read_tls_private_key(self, private_key_path):
private_key_pem = self.read_file(private_key_path, "tls_private_key")
return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
@classmethod
def generate_config(cls, args, config_dir_path):
super(TlsConfig, cls).generate_config(args, config_dir_path)
base_key_name = os.path.join(config_dir_path, args.server_name)
if args.tls_certificate_path is None:
args.tls_certificate_path = base_key_name + ".tls.crt"
if args.tls_private_key_path is None:
args.tls_private_key_path = base_key_name + ".tls.key"
if args.tls_dh_params_path is None:
args.tls_dh_params_path = base_key_name + ".tls.dh"
if not os.path.exists(args.tls_private_key_path):
with open(args.tls_private_key_path, "w") as private_key_file:
tls_private_key = crypto.PKey()
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
private_key_pem = crypto.dump_privatekey(
crypto.FILETYPE_PEM, tls_private_key
)
private_key_file.write(private_key_pem)
else:
with open(args.tls_private_key_path) as private_key_file:
private_key_pem = private_key_file.read()
tls_private_key = crypto.load_privatekey(
crypto.FILETYPE_PEM, private_key_pem
)
if not os.path.exists(args.tls_certificate_path):
with open(args.tls_certificate_path, "w") as certifcate_file:
cert = crypto.X509()
subject = cert.get_subject()
subject.CN = args.server_name
cert.set_serial_number(1000)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
cert.set_issuer(cert.get_subject())
cert.set_pubkey(tls_private_key)
cert.sign(tls_private_key, 'sha256')
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
certifcate_file.write(cert_pem)
if not os.path.exists(args.tls_dh_params_path):
if GENERATE_DH_PARAMS:
subprocess.check_call([
"openssl", "dhparam",
"-outform", "PEM",
"-out", args.tls_dh_params_path,
"2048"
])
else:
with open(args.tls_dh_params_path, "w") as dh_params_file:
dh_params_file.write(
"2048-bit DH parameters taken from rfc3526\n"
"-----BEGIN DH PARAMETERS-----\n"
"MIIBCAKCAQEA///////////JD9qiIWjC"
"NMTGYouA3BzRKQJOCIpnzHQCC76mOxOb\n"
"IlFKCHmONATd75UZs806QxswKwpt8l8U"
"N0/hNW1tUcJF5IW1dmJefsb0TELppjft\n"
"awv/XLb0Brft7jhr+1qJn6WunyQRfEsf"
"5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT\n"
"mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVS"
"u57VKQdwlpZtZww1Tkq8mATxdGwIyhgh\n"
"fDKQXkYuNs474553LBgOhgObJ4Oi7Aei"
"j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq\n"
"5RXSJhiY+gUQFXKOWoqsqmj/////////"
"/wIBAg==\n"
"-----END DH PARAMETERS-----\n"
)

View File

@@ -1,160 +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.
import ConfigParser as configparser
import argparse
import socket
import sys
import os
from OpenSSL import crypto
import nacl.signing
from syutil.base64util import encode_base64
import subprocess
def load_config(description, argv):
config_parser = argparse.ArgumentParser(add_help=False)
config_parser.add_argument("-c", "--config-path", metavar="CONFIG_FILE",
help="Specify config file")
config_args, remaining_args = config_parser.parse_known_args(argv)
if config_args.config_path:
config = configparser.SafeConfigParser()
config.read([config_args.config_path])
defaults = dict(config.items("KeyServer"))
else:
defaults = {}
parser = argparse.ArgumentParser(
parents=[config_parser],
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.set_defaults(**defaults)
parser.add_argument("--server-name", default=socket.getfqdn(),
help="The name of the server")
parser.add_argument("--signing-key-path",
help="The signing key to sign responses with")
parser.add_argument("--tls-certificate-path",
help="PEM encoded X509 certificate for TLS")
parser.add_argument("--tls-private-key-path",
help="PEM encoded private key for TLS")
parser.add_argument("--tls-dh-params-path",
help="PEM encoded dh parameters for ephemeral keys")
parser.add_argument("--bind-port", type=int,
help="TCP port to listen on")
parser.add_argument("--bind-host", default="",
help="Local interface to listen on")
args = parser.parse_args(remaining_args)
server_config = vars(args)
del server_config["config_path"]
return server_config
def generate_config(argv):
parser = argparse.ArgumentParser()
parser.add_argument("-c", "--config-path", help="Specify config file",
metavar="CONFIG_FILE", required=True)
parser.add_argument("--server-name", default=socket.getfqdn(),
help="The name of the server")
parser.add_argument("--signing-key-path",
help="The signing key to sign responses with")
parser.add_argument("--tls-certificate-path",
help="PEM encoded X509 certificate for TLS")
parser.add_argument("--tls-private-key-path",
help="PEM encoded private key for TLS")
parser.add_argument("--tls-dh-params-path",
help="PEM encoded dh parameters for ephemeral keys")
parser.add_argument("--bind-port", type=int, required=True,
help="TCP port to listen on")
parser.add_argument("--bind-host", default="",
help="Local interface to listen on")
args = parser.parse_args(argv)
dir_name = os.path.dirname(args.config_path)
base_key_name = os.path.join(dir_name, args.server_name)
if args.signing_key_path is None:
args.signing_key_path = base_key_name + ".signing.key"
if args.tls_certificate_path is None:
args.tls_certificate_path = base_key_name + ".tls.crt"
if args.tls_private_key_path is None:
args.tls_private_key_path = base_key_name + ".tls.key"
if args.tls_dh_params_path is None:
args.tls_dh_params_path = base_key_name + ".tls.dh"
if not os.path.exists(args.signing_key_path):
with open(args.signing_key_path, "w") as signing_key_file:
key = nacl.signing.SigningKey.generate()
signing_key_file.write(encode_base64(key.encode()))
if not os.path.exists(args.tls_private_key_path):
with open(args.tls_private_key_path, "w") as private_key_file:
tls_private_key = crypto.PKey()
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
private_key_pem = crypto.dump_privatekey(
crypto.FILETYPE_PEM, tls_private_key
)
private_key_file.write(private_key_pem)
else:
with open(args.tls_private_key_path) as private_key_file:
private_key_pem = private_key_file.read()
tls_private_key = crypto.load_privatekey(
crypto.FILETYPE_PEM, private_key_pem
)
if not os.path.exists(args.tls_certificate_path):
with open(args.tls_certificate_path, "w") as certifcate_file:
cert = crypto.X509()
subject = cert.get_subject()
subject.CN = args.server_name
cert.set_serial_number(1000)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
cert.set_issuer(cert.get_subject())
cert.set_pubkey(tls_private_key)
cert.sign(tls_private_key, 'sha256')
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
certifcate_file.write(cert_pem)
if not os.path.exists(args.tls_dh_params_path):
subprocess.check_call([
"openssl", "dhparam",
"-outform", "PEM",
"-out", args.tls_dh_params_path,
"2048"
])
config = configparser.SafeConfigParser()
config.add_section("KeyServer")
for key, value in vars(args).items():
if key != "config_path":
config.set("KeyServer", key, str(value))
with open(args.config_path, "w") as config_file:
config.write(config_file)
if __name__ == "__main__":
generate_config(sys.argv[1:])

View File

@@ -0,0 +1,29 @@
from twisted.internet import reactor, ssl
from OpenSSL import SSL
from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName
class ServerContextFactory(ssl.ContextFactory):
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
connections and to make connections to remote servers."""
def __init__(self, config):
self._context = SSL.Context(SSL.SSLv23_METHOD)
self.configure_context(self._context, config)
@staticmethod
def configure_context(context, config):
try:
_ecCurve = _OpenSSLECCurve(_defaultCurveName)
_ecCurve.addECKeyToContext(context)
except:
pass
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
context.use_certificate(config.tls_certificate)
context.use_privatekey(config.tls_private_key)
context.load_tmp_dh(config.tls_dh_params_path)
context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
def getContext(self):
return self._context

View File

@@ -68,6 +68,7 @@ class Pdu(JsonEncodedObject):
"power_level",
"prev_state_id",
"prev_state_origin",
"required_power_level",
]
internal_keys = [

View File

@@ -76,6 +76,10 @@ class MessageHandler(BaseRoomHandler):
Raises:
SynapseError if something went wrong.
"""
# TODO(paul): Why does 'event' not have a 'user' object?
user = self.hs.parse_userid(event.user_id)
assert user.is_mine, "User must be our own: %s" % (user,)
if stamp_event:
event.content["hsob_ts"] = int(self.clock.time_msec())
@@ -86,6 +90,10 @@ class MessageHandler(BaseRoomHandler):
yield self._on_new_room_event(event, snapshot)
self.hs.get_handlers().presence_handler.bump_presence_active_time(
user
)
@defer.inlineCallbacks
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
feedback=False):
@@ -274,11 +282,11 @@ class MessageHandler(BaseRoomHandler):
messages, token = yield self.store.get_recent_events_for_room(
event.room_id,
limit=limit,
end_token=now_token.events_key,
end_token=now_token.room_key,
)
start_token = now_token.copy_and_replace("events_key", token[0])
end_token = now_token.copy_and_replace("events_key", token[1])
start_token = now_token.copy_and_replace("room_key", token[0])
end_token = now_token.copy_and_replace("room_key", token[1])
d["messages"] = {
"chunk": [m.get_dict() for m in messages],

View File

@@ -52,6 +52,13 @@ def partitionbool(l, func):
class PresenceHandler(BaseHandler):
STATE_LEVELS = {
PresenceState.OFFLINE: 0,
PresenceState.UNAVAILABLE: 1,
PresenceState.ONLINE: 2,
PresenceState.FREE_FOR_CHAT: 3,
}
def __init__(self, hs):
super(PresenceHandler, self).__init__(hs)
@@ -135,7 +142,7 @@ class PresenceHandler(BaseHandler):
return self._user_cachemap[user]
else:
statuscache = UserPresenceCache()
statuscache.update({"state": PresenceState.OFFLINE}, user)
statuscache.update({"presence": PresenceState.OFFLINE}, user)
return statuscache
def registered_user(self, user):
@@ -143,10 +150,6 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks
def is_presence_visible(self, observer_user, observed_user):
defer.returnValue(True)
# return
# FIXME (erikj): This code path absolutely kills the database.
assert(observed_user.is_mine)
if observer_user == observed_user:
@@ -173,19 +176,24 @@ class PresenceHandler(BaseHandler):
observed_user=target_user
)
if visible:
state = yield self.store.get_presence_state(
target_user.localpart
)
else:
if not visible:
raise SynapseError(404, "Presence information not visible")
state = yield self.store.get_presence_state(target_user.localpart)
if "mtime" in state:
del state["mtime"]
state["presence"] = state["state"]
if target_user in self._user_cachemap:
state["last_active"] = (
self._user_cachemap[target_user].get_state()["last_active"]
)
else:
# TODO(paul): Have remote server send us permissions set
state = self._get_or_offline_usercache(target_user).get_state()
if "mtime" in state and (state["mtime"] is not None):
state["mtime_age"] = int(
self.clock.time_msec() - state.pop("mtime")
if "last_active" in state:
state["last_active_ago"] = int(
self.clock.time_msec() - state.pop("last_active")
)
defer.returnValue(state)
@@ -202,20 +210,33 @@ class PresenceHandler(BaseHandler):
if target_user != auth_user:
raise AuthError(400, "Cannot set another user's displayname")
# TODO(paul): Sanity-check 'state'
if "status_msg" not in state:
state["status_msg"] = None
for k in state.keys():
if k not in ("state", "status_msg"):
if k not in ("presence", "state", "status_msg"):
raise SynapseError(
400, "Unexpected presence state key '%s'" % (k,)
)
# Handle legacy "state" key for now
if "state" in state:
state["presence"] = state.pop("state")
if state["presence"] not in self.STATE_LEVELS:
raise SynapseError(400, "'%s' is not a valid presence state" %
state["presence"]
)
logger.debug("Updating presence state of %s to %s",
target_user.localpart, state["state"])
target_user.localpart, state["presence"])
state_to_store = dict(state)
state_to_store["state"] = state_to_store.pop("presence")
statuscache=self._get_or_offline_usercache(target_user)
was_level = self.STATE_LEVELS[statuscache.get_state()["presence"]]
now_level = self.STATE_LEVELS[state["presence"]]
yield defer.DeferredList([
self.store.set_presence_state(
@@ -226,9 +247,10 @@ class PresenceHandler(BaseHandler):
),
])
state["mtime"] = self.clock.time_msec()
if now_level > was_level:
state["last_active"] = self.clock.time_msec()
now_online = state["state"] != PresenceState.OFFLINE
now_online = state["presence"] != PresenceState.OFFLINE
was_polling = target_user in self._user_cachemap
if now_online and not was_polling:
@@ -240,6 +262,12 @@ class PresenceHandler(BaseHandler):
# we don't have to do this all the time
self.changed_presencelike_data(target_user, state)
def bump_presence_active_time(self, user, now=None):
if now is None:
now = self.clock.time_msec()
self.changed_presencelike_data(user, {"last_active": now})
def changed_presencelike_data(self, user, state):
statuscache = self._get_or_make_usercache(user)
@@ -251,28 +279,27 @@ class PresenceHandler(BaseHandler):
@log_function
def started_user_eventstream(self, user):
# TODO(paul): Use "last online" state
self.set_state(user, user, {"state": PresenceState.ONLINE})
self.set_state(user, user, {"presence": 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})
self.set_state(user, user, {"presence": PresenceState.OFFLINE})
@defer.inlineCallbacks
def user_joined_room(self, user, room_id):
if user.is_mine:
statuscache = self._get_or_make_usercache(user)
# No actual update but we need to bump the serial anyway for the
# event source
self._user_cachemap_latest_serial += 1
statuscache.update({}, serial=self._user_cachemap_latest_serial)
self.push_update_to_local_and_remote(
observed_user=user,
room_ids=[room_id],
statuscache=self._get_or_offline_usercache(user),
)
else:
self.push_update_to_clients(
observed_user=user,
room_ids=[room_id],
statuscache=self._get_or_offline_usercache(user),
statuscache=statuscache,
)
# We also want to tell them about current presence of people.
@@ -386,9 +413,9 @@ class PresenceHandler(BaseHandler):
observed_user = self.hs.parse_userid(p.pop("observed_user_id"))
p["observed_user"] = observed_user
p.update(self._get_or_offline_usercache(observed_user).get_state())
if "mtime" in p:
p["mtime_age"] = int(
self.clock.time_msec() - p.pop("mtime")
if "last_active" in p:
p["last_active_ago"] = int(
self.clock.time_msec() - p.pop("last_active")
)
defer.returnValue(presence)
@@ -457,10 +484,6 @@ class PresenceHandler(BaseHandler):
def _start_polling_local(self, user, target_user):
target_localpart = target_user.localpart
if not self.is_presence_visible(observer_user=user,
observed_user=target_user):
return
if target_localpart not in self._local_pushmap:
self._local_pushmap[target_localpart] = set()
@@ -577,21 +600,30 @@ class PresenceHandler(BaseHandler):
def _push_presence_remote(self, user, destination, state=None):
if state is None:
state = yield self.store.get_presence_state(user.localpart)
del state["mtime"]
state["presence"] = state["state"]
if user in self._user_cachemap:
state["last_active"] = (
self._user_cachemap[user].get_state()["last_active"]
)
yield self.distributor.fire(
"collect_presencelike_data", user, state
)
if "mtime" in state:
if "last_active" in state:
state = dict(state)
state["mtime_age"] = int(
self.clock.time_msec() - state.pop("mtime")
state["last_active_ago"] = int(
self.clock.time_msec() - state.pop("last_active")
)
user_state = {
"user_id": user.to_string(),
}
user_state.update(**state)
if "state" in user_state and "presence" not in user_state:
user_state["presence"] = user_state["state"]
yield self.federation.send_edu(
destination=destination,
@@ -618,14 +650,29 @@ class PresenceHandler(BaseHandler):
room_ids = yield rm_handler.get_rooms_for_user(user)
if not observers and not room_ids:
break
continue
state = dict(push)
del state["user_id"]
if "mtime_age" in state:
state["mtime"] = int(
self.clock.time_msec() - state.pop("mtime_age")
if "presence" in state:
# all is OK
pass
elif "state" in state:
# Legacy handling
state["presence"] = state["state"]
else:
logger.warning("Received a presence 'push' EDU from %s without"
+ " either a 'presence' or 'state' key", origin
)
continue
if "state" in state:
del state["state"]
if "last_active_ago" in state:
state["last_active"] = int(
self.clock.time_msec() - state.pop("last_active_ago")
)
statuscache = self._get_or_make_usercache(user)
@@ -640,7 +687,7 @@ class PresenceHandler(BaseHandler):
statuscache=statuscache,
)
if state["state"] == PresenceState.OFFLINE:
if state["presence"] == PresenceState.OFFLINE:
del self._user_cachemap[user]
for poll in content.get("poll", []):
@@ -673,10 +720,9 @@ class PresenceHandler(BaseHandler):
yield defer.DeferredList(deferreds)
@defer.inlineCallbacks
def push_update_to_local_and_remote(self, observed_user,
def push_update_to_local_and_remote(self, observed_user, statuscache,
users_to_push=[], room_ids=[],
remote_domains=[],
statuscache=None):
remote_domains=[]):
localusers, remoteusers = partitionbool(
users_to_push,
@@ -722,6 +768,78 @@ class PresenceHandler(BaseHandler):
)
class PresenceEventSource(object):
def __init__(self, hs):
self.hs = hs
self.clock = hs.get_clock()
def get_new_events_for_user(self, user, from_key, limit):
from_key = int(from_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]
return ((data, latest_serial))
else:
return (([], presence._user_cachemap_latest_serial))
def get_current_key(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 UserPresenceCache(object):
"""Store an observed user's state and status message.
@@ -733,6 +851,7 @@ class UserPresenceCache(object):
def update(self, state, serial):
assert("mtime_age" not in state)
assert("state" not in state)
self.state.update(state)
# Delete keys that are now 'None'
@@ -749,15 +868,21 @@ class UserPresenceCache(object):
def get_state(self):
# clone it so caller can't break our cache
return dict(self.state)
state = dict(self.state)
# Legacy handling
if "presence" in state:
state["state"] = state["presence"]
return state
def make_event(self, user, clock):
content = self.get_state()
content["user_id"] = user.to_string()
if "mtime" in content:
content["mtime_age"] = int(
clock.time_msec() - content.pop("mtime")
if "last_active" in content:
content["last_active_ago"] = int(
clock.time_msec() - content.pop("last_active")
)
return {"type": "m.presence", "content": content}

View File

@@ -17,10 +17,12 @@
from twisted.internet import defer
from synapse.types import UserID, RoomAlias, RoomID
from synapse.api.constants import Membership
from synapse.api.constants import Membership, JoinRules
from synapse.api.errors import StoreError, SynapseError
from synapse.api.events.room import (
RoomMemberEvent, RoomConfigEvent
RoomMemberEvent, RoomCreateEvent, RoomPowerLevelsEvent,
RoomJoinRulesEvent, RoomAddStateLevelEvent, RoomTopicEvent,
RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, RoomNameEvent,
)
from synapse.util import stringutils
from ._base import BaseRoomHandler
@@ -62,6 +64,8 @@ class RoomCreationHandler(BaseRoomHandler):
else:
room_alias = None
is_public = config.get("visibility", None) == "public"
if room_id:
# Ensure room_id is the correct type
room_id_obj = RoomID.from_string(room_id, self.hs)
@@ -71,7 +75,7 @@ class RoomCreationHandler(BaseRoomHandler):
yield self.store.store_room(
room_id=room_id,
room_creator_user_id=user_id,
is_public=config["visibility"] == "public"
is_public=is_public
)
else:
# autogen room IDs and try to create it. We may clash, so just
@@ -85,7 +89,7 @@ class RoomCreationHandler(BaseRoomHandler):
yield self.store.store_room(
room_id=gen_room_id.to_string(),
room_creator_user_id=user_id,
is_public=config["visibility"] == "public"
is_public=is_public
)
room_id = gen_room_id.to_string()
break
@@ -94,18 +98,9 @@ class RoomCreationHandler(BaseRoomHandler):
if not room_id:
raise StoreError(500, "Couldn't generate a room ID.")
config_event = self.event_factory.create_event(
etype=RoomConfigEvent.TYPE,
room_id=room_id,
user_id=user_id,
content=config,
)
snapshot = yield self.store.snapshot_room(
room_id=room_id,
user_id=user_id,
state_type=RoomConfigEvent.TYPE,
state_key="",
user = self.hs.parse_userid(user_id)
creation_events = self._create_events_for_new_room(
user, room_id, is_public=is_public
)
if room_alias:
@@ -115,11 +110,46 @@ class RoomCreationHandler(BaseRoomHandler):
servers=[self.hs.hostname],
)
yield self.state_handler.handle_new_event(config_event, snapshot)
# store_id = persist...
federation_handler = self.hs.get_handlers().federation_handler
yield federation_handler.handle_new_event(config_event, snapshot)
@defer.inlineCallbacks
def handle_event(event):
snapshot = yield self.store.snapshot_room(
room_id=room_id,
user_id=user_id,
)
logger.debug("Event: %s", event)
yield self.state_handler.handle_new_event(event, snapshot)
yield self._on_new_room_event(event, snapshot, extra_users=[user])
for event in creation_events:
yield handle_event(event)
if "name" in config:
name = config["name"]
name_event = self.event_factory.create_event(
etype=RoomNameEvent.TYPE,
room_id=room_id,
user_id=user_id,
required_power_level=5,
content={"name": name},
)
yield handle_event(name_event)
if "topic" in config:
topic = config["topic"]
topic_event = self.event_factory.create_event(
etype=RoomTopicEvent.TYPE,
room_id=room_id,
user_id=user_id,
required_power_level=5,
content={"topic": topic},
)
yield handle_event(topic_event)
content = {"membership": Membership.JOIN}
join_event = self.event_factory.create_event(
@@ -142,6 +172,62 @@ class RoomCreationHandler(BaseRoomHandler):
defer.returnValue(result)
def _create_events_for_new_room(self, creator, room_id, is_public=False):
event_keys = {
"room_id": room_id,
"user_id": creator.to_string(),
"required_power_level": 10,
}
def create(etype, **content):
return self.event_factory.create_event(
etype=etype,
content=content,
**event_keys
)
creation_event = create(
etype=RoomCreateEvent.TYPE,
creator=creator.to_string(),
)
power_levels_event = self.event_factory.create_event(
etype=RoomPowerLevelsEvent.TYPE,
content={creator.to_string(): 10, "default": 0},
**event_keys
)
join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE
join_rules_event = create(
etype=RoomJoinRulesEvent.TYPE,
join_rule=join_rule,
)
add_state_event = create(
etype=RoomAddStateLevelEvent.TYPE,
level=10,
)
send_event = create(
etype=RoomSendEventLevelEvent.TYPE,
level=0,
)
ops = create(
etype=RoomOpsPowerLevelsEvent.TYPE,
ban_level=5,
kick_level=5,
)
return [
creation_event,
power_levels_event,
join_rules_event,
add_state_event,
send_event,
ops,
]
class RoomMemberHandler(BaseRoomHandler):
# TODO(paul): This handler currently contains a messy conflation of
@@ -285,6 +371,16 @@ class RoomMemberHandler(BaseRoomHandler):
if do_auth:
yield self.auth.check(event, snapshot, raises=True)
# If we're banning someone, set a req power level
if event.membership == Membership.BAN:
if not hasattr(event, "required_power_level") or event.required_power_level is None:
# Add some default required_power_level
user_level = yield self.store.get_power_level(
event.room_id,
event.user_id,
)
event.required_power_level = user_level
if prev_state and prev_state.membership == event.membership:
# double same action, treat this event as a NOOP.
defer.returnValue({})
@@ -445,10 +541,9 @@ class RoomMemberHandler(BaseRoomHandler):
host = target_user.domain
destinations.append(host)
# If we are joining a remote HS, include that.
if membership == Membership.JOIN:
host = target_user.domain
destinations.append(host)
# Always include target domain
host = target_user.domain
destinations.append(host)
return self._on_new_room_event(
event, snapshot, extra_destinations=destinations,
@@ -462,3 +557,49 @@ class RoomListHandler(BaseRoomHandler):
chunk = yield self.store.get_rooms(is_public=True)
# FIXME (erikj): START is no longer a valid value
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
class RoomEventSource(object):
def __init__(self, hs):
self.store = hs.get_datastore()
@defer.inlineCallbacks
def get_new_events_for_user(self, user, from_key, limit):
# We just ignore the key for now.
to_key = yield self.get_current_key()
events, end_key = yield self.store.get_room_events_stream(
user_id=user.to_string(),
from_key=from_key,
to_key=to_key,
room_id=None,
limit=limit,
)
defer.returnValue((events, end_key))
def get_current_key(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.room_key if to_token else None
events, next_key = yield self.store.paginate_room_events(
room_id=key,
from_key=from_token.room_key,
to_key=to_key,
direction=direction,
limit=limit,
with_feedback=True
)
next_token = from_token.copy_and_replace("room_key", next_key)
defer.returnValue((events, next_token))

View File

@@ -145,3 +145,17 @@ class TypingNotificationHandler(BaseHandler):
typing):
# TODO(paul) steal this from presence.py
pass
class TypingNotificationEventSource(object):
def __init__(self, hs):
self.hs = hs
def get_new_events_for_user(self, user, from_key, limit):
return ([], from_key)
def get_current_key(self):
return 0
def get_pagination_rows(self, user, pagination_config, key):
return ([], pagination_config.from_token)

View File

@@ -113,8 +113,9 @@ class TwistedHttpClient(HttpClient):
requests.
"""
def __init__(self):
def __init__(self, hs):
self.agent = MatrixHttpAgent(reactor)
self.hs = hs
@defer.inlineCallbacks
def put_json(self, destination, path, data):
@@ -177,7 +178,10 @@ class TwistedHttpClient(HttpClient):
retries_left = 5
# TODO: setup and pass in an ssl_context to enable TLS
endpoint = matrix_endpoint(reactor, destination, timeout=10)
endpoint = matrix_endpoint(
reactor, destination, timeout=10,
ssl_context_factory=self.hs.tls_context_factory
)
while True:
try:

View File

@@ -50,11 +50,11 @@ def matrix_endpoint(reactor, destination, ssl_context_factory=None,
if ssl_context_factory is None:
transport_endpoint = TCP4ClientEndpoint
default_port = 8080
default_port = 8008
else:
transport_endpoint = SSL4ClientEndpoint
endpoint_kw_args.update(ssl_context_factory=ssl_context_factory)
default_port = 443
endpoint_kw_args.update(sslContextFactory=ssl_context_factory)
default_port = 8448
if port is None:
return SRVClientEndpoint(

View File

@@ -325,7 +325,11 @@ class ContentRepoResource(resource.Resource):
# FIXME (erikj): These should use constants.
file_name = os.path.basename(fname)
url = "http://%s/matrix/content/%s" % (
# FIXME: we can't assume what the public mounted path of the repo is
# ...plus self-signed SSL won't work to remote clients anyway
# ...and we can't assume that it's SSL anyway, as we might want to
# server it via the non-SSL listener...
url = "https://%s/_matrix/content/%s" % (
self.hs.domain_with_port, file_name
)

View File

@@ -95,7 +95,7 @@ class Notifier(object):
"""
room_id = event.room_id
source = self.event_sources.sources["room"]
room_source = self.event_sources.sources["room"]
listeners = self.rooms_to_listeners.get(room_id, set()).copy()
@@ -109,13 +109,17 @@ class Notifier(object):
@defer.inlineCallbacks
def notify(listener):
events, end_token = yield source.get_new_events_for_user(
events, end_key = yield room_source.get_new_events_for_user(
listener.user,
listener.from_token,
listener.from_token.room_key,
listener.limit,
)
if events:
end_token = listener.from_token.copy_and_replace(
"room_key", end_key
)
listener.notify(
self, events, listener.from_token, end_token
)
@@ -135,7 +139,7 @@ class Notifier(object):
Will wake up all listeners for the given users and rooms.
"""
source = self.event_sources.sources["presence"]
presence_source = self.event_sources.sources["presence"]
listeners = set()
@@ -147,13 +151,17 @@ class Notifier(object):
@defer.inlineCallbacks
def notify(listener):
events, end_token = yield source.get_new_events_for_user(
events, end_key = yield presence_source.get_new_events_for_user(
listener.user,
listener.from_token,
listener.from_token.presence_key,
listener.limit,
)
if events:
end_token = listener.from_token.copy_and_replace(
"presence_key", end_key
)
listener.notify(
self, events, listener.from_token, end_token
)
@@ -233,16 +241,18 @@ class Notifier(object):
limit = listener.limit
# TODO (erikj): DeferredList?
for source in self.event_sources.sources.values():
stuff, new_token = yield source.get_new_events_for_user(
for name, source in self.event_sources.sources.items():
keyname = "%s_key" % name
stuff, new_key = yield source.get_new_events_for_user(
listener.user,
from_token,
getattr(from_token, keyname),
limit,
)
events.extend(stuff)
from_token = new_token
from_token = from_token.copy_and_replace(keyname, new_key)
end_token = from_token

View File

@@ -48,7 +48,11 @@ class PresenceStatusRestServlet(RestServlet):
try:
content = json.loads(request.content.read())
state["state"] = content.pop("state")
# Legacy handling
if "state" in content:
state["presence"] = content.pop("state")
else:
state["presence"] = content.pop("presence")
if "status_msg" in content:
state["status_msg"] = content.pop("status_msg")

View File

@@ -87,6 +87,27 @@ class ProfileAvatarURLRestServlet(RestServlet):
return (200, {})
class ProfileRestServlet(RestServlet):
PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)")
@defer.inlineCallbacks
def on_GET(self, request, user_id):
user = self.hs.parse_userid(user_id)
displayname = yield self.handlers.profile_handler.get_displayname(
user,
)
avatar_url = yield self.handlers.profile_handler.get_avatar_url(
user,
)
defer.returnValue((200, {
"displayname": displayname,
"avatar_url": avatar_url
}))
def register_servlets(hs, http_server):
ProfileDisplaynameRestServlet(hs).register(http_server)
ProfileAvatarURLRestServlet(hs).register(http_server)
ProfileRestServlet(hs).register(http_server)

View File

@@ -19,6 +19,11 @@ from synapse.api.events.room import (
RoomMemberEvent, RoomTopicEvent, FeedbackEvent,
# RoomConfigEvent,
RoomNameEvent,
RoomJoinRulesEvent,
RoomPowerLevelsEvent,
RoomAddStateLevelEvent,
RoomSendEventLevelEvent,
RoomOpsPowerLevelsEvent,
)
from synapse.util.logutils import log_function
@@ -33,6 +38,7 @@ from .roommember import RoomMemberStore
from .stream import StreamStore
from .pdu import StatePduStore, PduStore
from .transactions import TransactionStore
from .keys import KeyStore
import json
import logging
@@ -45,7 +51,7 @@ logger = logging.getLogger(__name__)
class DataStore(RoomMemberStore, RoomStore,
RegistrationStore, StreamStore, ProfileStore, FeedbackStore,
PresenceStore, PduStore, StatePduStore, TransactionStore,
DirectoryStore):
DirectoryStore, KeyStore):
def __init__(self, hs):
super(DataStore, self).__init__(hs)
@@ -122,13 +128,21 @@ class DataStore(RoomMemberStore, RoomStore,
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)
self._store_feedback_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)
elif event.type == RoomJoinRulesEvent.TYPE:
self._store_join_rule(txn, event)
elif event.type == RoomPowerLevelsEvent.TYPE:
self._store_power_levels(txn, event)
elif event.type == RoomAddStateLevelEvent.TYPE:
self._store_add_state_level(txn, event)
elif event.type == RoomSendEventLevelEvent.TYPE:
self._store_send_event_level(txn, event)
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
self._store_ops_level(txn, event)
vals = {
"topological_ordering": event.depth,
@@ -222,7 +236,6 @@ 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:

103
synapse/storage/keys.py Normal file
View File

@@ -0,0 +1,103 @@
# -*- 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 _base import SQLBaseStore
from twisted.internet import defer
import OpenSSL
import nacl.signing
class KeyStore(SQLBaseStore):
"""Persistence for signature verification keys and tls X.509 certificates
"""
@defer.inlineCallbacks
def get_server_certificate(self, server_name):
"""Retrieve the TLS X.509 certificate for the given server
Args:
server_name (bytes): The name of the server.
Returns:
(OpenSSL.crypto.X509): The tls certificate.
"""
tls_certificate_bytes, = yield self._simple_select_one(
table="server_tls_certificates",
keyvalues={"server_name": server_name},
retcols=("tls_certificate",),
)
tls_certificate = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_ASN1, tls_certificate_bytes,
)
defer.returnValue(tls_certificate)
def store_server_certificate(self, server_name, key_server, ts_now_ms,
tls_certificate):
"""Stores the TLS X.509 certificate for the given server
Args:
server_name (bytes): The name of the server.
key_server (bytes): Where the certificate was looked up
ts_now_ms (int): The time now in milliseconds
tls_certificate (OpenSSL.crypto.X509): The X.509 certificate.
"""
tls_certificate_bytes = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, tls_certificate
)
return self._simple_insert(
table="server_tls_certificates",
keyvalues={
"server_name": server_name,
"key_server": key_server,
"ts_added_ms": ts_now_ms,
"tls_certificate": tls_certificate_bytes,
},
)
@defer.inlineCallbacks
def get_server_verification_key(self, server_name):
"""Retrieve the NACL verification key for a given server
Args:
server_name (bytes): The name of the server.
Returns:
(nacl.signing.VerifyKey): The verification key.
"""
verification_key_bytes, = yield self._simple_select_one(
table="server_signature_keys",
key_values={"server_name": server_name},
retcols=("tls_certificate",),
)
verification_key = nacl.signing.VerifyKey(verification_key_bytes)
defer.returnValue(verification_key)
def store_server_verification_key(self, server_name, key_version,
key_server, ts_now_ms, verification_key):
"""Stores a NACL verification key for the given server.
Args:
server_name (bytes): The name of the server.
key_version (bytes): The version of the key for the server.
key_server (bytes): Where the verification key was looked up
ts_now_ms (int): The time now in milliseconds
verification_key (nacl.signing.VerifyKey): The NACL verify key.
"""
verification_key_bytes = verification_key.encode()
return self._simple_insert(
table="server_signature_keys",
key_values={
"server_name": server_name,
"key_version": key_version,
"key_server": key_server,
"ts_added_ms": ts_now_ms,
"verification_key": verification_key_bytes,
},
)

View File

@@ -27,6 +27,9 @@ import logging
logger = logging.getLogger(__name__)
OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level"))
class RoomStore(SQLBaseStore):
@defer.inlineCallbacks
@@ -129,6 +132,98 @@ class RoomStore(SQLBaseStore):
defer.returnValue(ret)
@defer.inlineCallbacks
def get_room_join_rule(self, room_id):
sql = (
"SELECT join_rule FROM room_join_rules as r "
"INNER JOIN current_state_events as c "
"ON r.event_id = c.event_id "
"WHERE c.room_id = ? "
)
rows = yield self._execute(None, sql, room_id)
if len(rows) == 1:
defer.returnValue(rows[0][0])
else:
defer.returnValue(None)
def get_power_level(self, room_id, user_id):
return self._db_pool.runInteraction(
self._get_power_level,
room_id, user_id,
)
def _get_power_level(self, txn, room_id, user_id):
sql = (
"SELECT level FROM room_power_levels as r "
"INNER JOIN current_state_events as c "
"ON r.event_id = c.event_id "
"WHERE c.room_id = ? AND r.user_id = ? "
)
rows = txn.execute(sql, (room_id, user_id,)).fetchall()
if len(rows) == 1:
return rows[0][0]
sql = (
"SELECT level FROM room_default_levels as r "
"INNER JOIN current_state_events as c "
"ON r.event_id = c.event_id "
"WHERE c.room_id = ? "
)
rows = txn.execute(sql, (room_id,)).fetchall()
if len(rows) == 1:
return rows[0][0]
else:
return None
def get_ops_levels(self, room_id):
return self._db_pool.runInteraction(
self._get_ops_levels,
room_id,
)
def _get_ops_levels(self, txn, room_id):
sql = (
"SELECT ban_level, kick_level FROM room_ops_levels as r "
"INNER JOIN current_state_events as c "
"ON r.event_id = c.event_id "
"WHERE c.room_id = ? "
)
rows = txn.execute(sql, (room_id,)).fetchall()
if len(rows) == 1:
return OpsLevel(rows[0][0], rows[0][1])
else:
return OpsLevel(None, None)
def get_add_state_level(self, room_id):
return self._get_level_from_table("room_add_state_levels", room_id)
def get_send_event_level(self, room_id):
return self._get_level_from_table("room_send_event_levels", room_id)
@defer.inlineCallbacks
def _get_level_from_table(self, table, room_id):
sql = (
"SELECT level FROM %(table)s as r "
"INNER JOIN current_state_events as c "
"ON r.event_id = c.event_id "
"WHERE c.room_id = ? "
) % {"table": table}
rows = yield self._execute(None, sql, room_id)
if len(rows) == 1:
defer.returnValue(rows[0][0])
else:
defer.returnValue(None)
def _store_room_topic_txn(self, txn, event):
self._simple_insert_txn(
txn,
@@ -151,6 +246,92 @@ class RoomStore(SQLBaseStore):
}
)
def _store_join_rule(self, txn, event):
self._simple_insert_txn(
txn,
"room_join_rules",
{
"event_id": event.event_id,
"room_id": event.room_id,
"join_rule": event.content["join_rule"],
},
)
def _store_power_levels(self, txn, event):
for user_id, level in event.content.items():
if user_id == "default":
self._simple_insert_txn(
txn,
"room_default_levels",
{
"event_id": event.event_id,
"room_id": event.room_id,
"level": level,
},
)
else:
self._simple_insert_txn(
txn,
"room_power_levels",
{
"event_id": event.event_id,
"room_id": event.room_id,
"user_id": user_id,
"level": level
},
)
def _store_default_level(self, txn, event):
self._simple_insert_txn(
txn,
"room_default_levels",
{
"event_id": event.event_id,
"room_id": event.room_id,
"level": event.content["default_level"],
},
)
def _store_add_state_level(self, txn, event):
self._simple_insert_txn(
txn,
"room_add_state_levels",
{
"event_id": event.event_id,
"room_id": event.room_id,
"level": event.content["level"],
},
)
def _store_send_event_level(self, txn, event):
self._simple_insert_txn(
txn,
"room_send_event_levels",
{
"event_id": event.event_id,
"room_id": event.room_id,
"level": event.content["level"],
},
)
def _store_ops_level(self, txn, event):
content = {
"event_id": event.event_id,
"room_id": event.room_id,
}
if "kick_level" in event.content:
content["kick_level"] = event.content["kick_level"]
if "ban_level" in event.content:
content["ban_level"] = event.content["ban_level"]
self._simple_insert_txn(
txn,
"room_ops_levels",
content,
)
class RoomsTable(Table):
table_name = "rooms"

View File

@@ -96,8 +96,71 @@ CREATE TABLE IF NOT EXISTS rooms(
creator TEXT
);
CREATE TABLE IF NOT EXISTS room_join_rules(
event_id TEXT NOT NULL,
room_id TEXT NOT NULL,
join_rule TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS room_join_rules_event_id ON room_join_rules(event_id);
CREATE INDEX IF NOT EXISTS room_join_rules_room_id ON room_join_rules(room_id);
CREATE TABLE IF NOT EXISTS room_power_levels(
event_id TEXT NOT NULL,
room_id TEXT NOT NULL,
user_id TEXT NOT NULL,
level INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS room_power_levels_event_id ON room_power_levels(event_id);
CREATE INDEX IF NOT EXISTS room_power_levels_room_id ON room_power_levels(room_id);
CREATE INDEX IF NOT EXISTS room_power_levels_room_user ON room_power_levels(room_id, user_id);
CREATE TABLE IF NOT EXISTS room_default_levels(
event_id TEXT NOT NULL,
room_id TEXT NOT NULL,
level INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS room_default_levels_event_id ON room_default_levels(event_id);
CREATE INDEX IF NOT EXISTS room_default_levels_room_id ON room_default_levels(room_id);
CREATE TABLE IF NOT EXISTS room_add_state_levels(
event_id TEXT NOT NULL,
room_id TEXT NOT NULL,
level INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS room_add_state_levels_event_id ON room_add_state_levels(event_id);
CREATE INDEX IF NOT EXISTS room_add_state_levels_room_id ON room_add_state_levels(room_id);
CREATE TABLE IF NOT EXISTS room_send_event_levels(
event_id TEXT NOT NULL,
room_id TEXT NOT NULL,
level INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS room_send_event_levels_event_id ON room_send_event_levels(event_id);
CREATE INDEX IF NOT EXISTS room_send_event_levels_room_id ON room_send_event_levels(room_id);
CREATE TABLE IF NOT EXISTS room_ops_levels(
event_id TEXT NOT NULL,
room_id TEXT NOT NULL,
ban_level INTEGER,
kick_level INTEGER
);
CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id);
CREATE INDEX IF NOT EXISTS room_ops_levels_room_id ON room_ops_levels(room_id);
CREATE TABLE IF NOT EXISTS room_hosts(
room_id TEXT NOT NULL,
host TEXT NOT NULL,
CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
);
CREATE INDEX IF NOT EXISTS room_hosts_room_id ON room_hosts (room_id);

View File

@@ -0,0 +1,30 @@
/* 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.
*/
CREATE TABLE IF NOT EXISTS server_tls_certificates(
server_name TEXT, -- Server name.
key_server TEXT, -- Which key server the certificate was fetched from.
ts_added_ms INTEGER, -- When the certifcate was added.
tls_certificate BLOB, -- DER encoded x509 certificate.
CONSTRAINT uniqueness UNIQUE (server_name)
);
CREATE TABLE IF NOT EXISTS server_signature_keys(
server_name TEXT, -- Server name.
key_version TEXT, -- Key version.
key_server TEXT, -- Which key server the key was fetched form.
ts_added_ms INTEGER, -- When the key was added.
verification_key BLOB, -- NACL verification key.
CONSTRAINT uniqueness UNIQUE (server_name, key_version)
);

View File

@@ -17,6 +17,10 @@ from twisted.internet import defer
from synapse.types import StreamToken
from synapse.handlers.presence import PresenceEventSource
from synapse.handlers.room import RoomEventSource
from synapse.handlers.typing import TypingNotificationEventSource
class NullSource(object):
"""This event source never yields any events and its token remains at
@@ -24,146 +28,21 @@ class NullSource(object):
def __init__(self, hs):
pass
def get_new_events_for_user(self, user, from_token, limit):
return defer.succeed(([], from_token))
def get_new_events_for_user(self, user, from_key, limit):
return defer.succeed(([], from_key))
def get_current_token_part(self):
def get_current_key(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,
"presence": PresenceEventSource,
"typing": TypingNotificationEventSource,
}
def __init__(self, hs):
@@ -172,24 +51,29 @@ class EventSources(object):
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)
token = StreamToken(
room_key=(
yield self.sources["room"].get_current_key()
),
presence_key=(
yield self.sources["presence"].get_current_key()
),
typing_key=(
yield self.sources["typing"].get_current_key()
)
)
defer.returnValue(token)
class StreamSource(object):
def get_new_events_for_user(self, user, from_token, limit):
def get_new_events_for_user(self, user, from_key, limit):
"""from_key is the key within this event source."""
raise NotImplementedError("get_new_events_for_user")
def get_current_token_part(self):
raise NotImplementedError("get_current_token_part")
def get_current_key(self):
raise NotImplementedError("get_current_key")
def get_pagination_rows(self, user, pagination_config, key):
raise NotImplementedError("get_rows")

View File

@@ -97,7 +97,7 @@ class RoomID(DomainSpecificString):
class StreamToken(
namedtuple(
"Token",
("events_key", "presence_key")
("room_key", "presence_key", "typing_key")
)
):
_SEPARATOR = "_"
@@ -105,21 +105,14 @@ class StreamToken(
@classmethod
def from_string(cls, string):
try:
events_key, presence_key = string.split(cls._SEPARATOR)
keys = string.split(cls._SEPARATOR)
return cls(
events_key=events_key,
presence_key=presence_key,
)
return cls(*keys)
except:
raise SynapseError(400, "Invalid Token")
def to_string(self):
return "".join([
str(self.events_key),
self._SEPARATOR,
str(self.presence_key),
])
return self._SEPARATOR.join([str(k) for k in self])
def copy_and_replace(self, key, new_value):
d = self._asdict()

View File

@@ -87,7 +87,7 @@ class FederationTestCase(unittest.TestCase):
# Empty context initially
(code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/state/my-context/", None)
"/_matrix/federation/v1/state/my-context/", None)
self.assertEquals(200, code)
self.assertFalse(response["pdus"])
@@ -112,7 +112,7 @@ class FederationTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/state/my-context/", None)
"/_matrix/federation/v1/state/my-context/", None)
self.assertEquals(200, code)
self.assertEquals(1, len(response["pdus"]))
@@ -123,7 +123,7 @@ class FederationTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/pdu/red/abc123def456/", None)
"/_matrix/federation/v1/pdu/red/abc123def456/", None)
self.assertEquals(404, code)
# Now insert such a PDU
@@ -142,7 +142,7 @@ class FederationTestCase(unittest.TestCase):
)
(code, response) = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/pdu/red/abc123def456/", None)
"/_matrix/federation/v1/pdu/red/abc123def456/", None)
self.assertEquals(200, code)
self.assertEquals(1, len(response["pdus"]))
self.assertEquals("m.text", response["pdus"][0]["pdu_type"])
@@ -168,7 +168,7 @@ class FederationTestCase(unittest.TestCase):
self.mock_http_client.put_json.assert_called_with(
"remote",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data={
"ts": 1000000,
"origin": "test",
@@ -203,7 +203,7 @@ class FederationTestCase(unittest.TestCase):
# MockClock ensures we can guess these timestamps
self.mock_http_client.put_json.assert_called_with(
"remote",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data={
"origin": "test",
"ts": 1000000,
@@ -226,7 +226,7 @@ class FederationTestCase(unittest.TestCase):
self.federation.register_edu_handler("m.test", recv_observer)
yield self.mock_resource.trigger("PUT",
"/matrix/federation/v1/send/1001000/",
"/_matrix/federation/v1/send/1001000/",
"""{
"origin": "remote",
"ts": 1001000,
@@ -261,7 +261,7 @@ class FederationTestCase(unittest.TestCase):
self.mock_http_client.get_json.assert_called_with(
destination="remote",
path="/matrix/federation/v1/query/a-question",
path="/_matrix/federation/v1/query/a-question",
args={"one": "1", "two": "2"}
)
@@ -273,7 +273,7 @@ class FederationTestCase(unittest.TestCase):
self.federation.register_query_handler("a-question", recv_handler)
code, response = yield self.mock_resource.trigger("GET",
"/matrix/federation/v1/query/a-question?three=3&four=4", None)
"/_matrix/federation/v1/query/a-question?three=3&four=4", None)
self.assertEquals(200, code)
self.assertEquals({"another": "response"}, response)

View File

@@ -35,8 +35,6 @@ ONLINE = PresenceState.ONLINE
logging.getLogger().addHandler(logging.NullHandler())
#logging.getLogger().addHandler(logging.StreamHandler())
#logging.getLogger().setLevel(logging.DEBUG)
def _expect_edu(destination, edu_type, content, origin="test"):
@@ -141,7 +139,8 @@ class PresenceStateTestCase(unittest.TestCase):
target_user=self.u_apple, auth_user=self.u_apple
)
self.assertEquals({"state": ONLINE, "status_msg": "Online"},
self.assertEquals(
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
state
)
mocked_get.assert_called_with("apple")
@@ -157,7 +156,8 @@ class PresenceStateTestCase(unittest.TestCase):
target_user=self.u_apple, auth_user=self.u_banana
)
self.assertEquals({"state": ONLINE, "status_msg": "Online"},
self.assertEquals(
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
state
)
mocked_get.assert_called_with("apple")
@@ -175,7 +175,10 @@ class PresenceStateTestCase(unittest.TestCase):
target_user=self.u_apple, auth_user=self.u_clementine
)
self.assertEquals({"state": ONLINE, "status_msg": "Online"}, state)
self.assertEquals(
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
state
)
@defer.inlineCallbacks
def test_get_disallowed_state(self):
@@ -193,8 +196,6 @@ class PresenceStateTestCase(unittest.TestCase):
SynapseError
)
test_get_disallowed_state.skip = "Presence permissions are disabled"
@defer.inlineCallbacks
def test_set_my_state(self):
mocked_set = self.datastore.set_presence_state
@@ -202,20 +203,20 @@ class PresenceStateTestCase(unittest.TestCase):
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"state": UNAVAILABLE, "status_msg": "Away"})
state={"presence": UNAVAILABLE, "status_msg": "Away"})
mocked_set.assert_called_with("apple",
{"state": UNAVAILABLE, "status_msg": "Away"})
self.mock_start.assert_called_with(self.u_apple,
state={
"state": UNAVAILABLE,
"presence": UNAVAILABLE,
"status_msg": "Away",
"mtime": 1000000, # MockClock
"last_active": 1000000, # MockClock
})
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"state": OFFLINE})
state={"presence": OFFLINE})
self.mock_stop.assert_called_with(self.u_apple)
@@ -314,7 +315,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("elsewhere",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_invite",
content={
"observer_user": "@apple:test",
@@ -340,7 +341,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("elsewhere",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_accept",
content={
"observer_user": "@cabbage:elsewhere",
@@ -352,7 +353,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_invite",
content={
"observer_user": "@cabbage:elsewhere",
@@ -371,7 +372,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("elsewhere",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("elsewhere", "m.presence_deny",
content={
"observer_user": "@cabbage:elsewhere",
@@ -383,7 +384,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_invite",
content={
"observer_user": "@cabbage:elsewhere",
@@ -397,7 +398,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_accepted_remote(self):
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_accept",
content={
"observer_user": "@apple:test",
@@ -415,7 +416,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_denied_remote(self):
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_deny",
content={
"observer_user": "@apple:test",
@@ -449,28 +450,35 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_get_presence_list(self):
self.datastore.get_presence_list.return_value = defer.succeed(
[{"observed_user_id": "@banana:test"}]
[{"observed_user_id": "@banana:test"}]
)
presence = yield self.handler.get_presence_list(
observer_user=self.u_apple)
self.assertEquals([{"observed_user": self.u_banana,
"state": OFFLINE}], presence)
self.assertEquals([
{"observed_user": self.u_banana,
"presence": OFFLINE,
"state": OFFLINE},
], presence)
self.datastore.get_presence_list.assert_called_with("apple",
accepted=None)
accepted=None
)
self.datastore.get_presence_list.return_value = defer.succeed(
[{"observed_user_id": "@banana:test"}]
[{"observed_user_id": "@banana:test"}]
)
presence = yield self.handler.get_presence_list(
observer_user=self.u_apple, accepted=True)
observer_user=self.u_apple, accepted=True
)
self.assertEquals([{"observed_user": self.u_banana,
"state": OFFLINE}], presence)
self.assertEquals([
{"observed_user": self.u_banana,
"presence": OFFLINE,
"state": OFFLINE},
], presence)
self.datastore.get_presence_list.assert_called_with("apple",
accepted=True)
@@ -514,13 +522,6 @@ 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.side_effect = update
self.datastore = hs.get_datastore()
def get_received_txn_response(*args):
@@ -528,7 +529,7 @@ class PresencePushTestCase(unittest.TestCase):
self.datastore.get_received_txn_response = get_received_txn_response
self.handler = hs.get_handlers().presence_handler
self.handler.push_update_to_clients = self.mock_update_client
self.event_source = hs.get_event_sources().sources["presence"]
# Mock the RoomMemberHandler
hs.handlers.room_member_handler = Mock(spec=[
@@ -618,31 +619,54 @@ class PresencePushTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
self.handler._user_cachemap[self.u_apple].update(
{"presence": OFFLINE}, serial=0
)
apple_set = self.handler._local_pushmap.setdefault("apple", set())
apple_set.add(self.u_banana)
apple_set.add(self.u_clementine)
yield self.handler.set_state(self.u_apple, self.u_apple,
{"state": ONLINE})
self.assertEquals(self.event_source.get_current_key(), 0)
self.mock_update_client.assert_has_calls([
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
], any_order=True)
self.mock_update_client.reset_mock()
yield self.handler.set_state(self.u_apple, self.u_apple,
{"presence": ONLINE}
)
(events, _) = yield self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)
self.assertEquals(self.event_source.get_current_key(), 1)
self.assertEquals(events,
[
{"type": "m.presence",
"content": {
"user_id": "@apple:test",
"presence": ONLINE,
"state": ONLINE,
"last_active_ago": 0,
}},
],
)
presence = yield self.handler.get_presence_list(
observer_user=self.u_apple, accepted=True)
self.assertEquals([
{"observed_user": self.u_banana, "state": OFFLINE},
{"observed_user": self.u_clementine, "state": OFFLINE}],
presence)
self.assertEquals(
[
{"observed_user": self.u_banana,
"presence": OFFLINE,
"state": OFFLINE},
{"observed_user": self.u_clementine,
"presence": OFFLINE,
"state": OFFLINE},
],
presence
)
yield self.handler.set_state(self.u_banana, self.u_banana,
{"state": ONLINE})
{"presence": ONLINE}
)
self.clock.advance_time(2)
@@ -651,37 +675,34 @@ class PresencePushTestCase(unittest.TestCase):
self.assertEquals([
{"observed_user": self.u_banana,
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 2000},
"last_active_ago": 2000},
{"observed_user": self.u_clementine,
"presence": OFFLINE,
"state": OFFLINE},
], presence)
self.mock_update_client.assert_has_calls([
call(users_to_push=set([self.u_banana]),
room_ids=[],
observed_user=self.u_banana,
statuscache=ANY), # self-reflection
]) # and no others...
(events, _) = yield self.event_source.get_new_events_for_user(
self.u_apple, 1, None
)
self.assertEquals(self.event_source.get_current_key(), 2)
self.assertEquals(events,
[
{"type": "m.presence",
"content": {
"user_id": "@banana:test",
"presence": ONLINE,
"state": ONLINE,
"last_active_ago": 2000
}},
]
)
@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("farm",
path=ANY, # Can't guarantee which txn ID will be which
@@ -689,8 +710,9 @@ class PresencePushTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@apple:test",
"presence": u"online",
"state": u"online",
"mtime_age": 0},
"last_active_ago": 0},
],
}
)
@@ -706,11 +728,14 @@ class PresencePushTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
self.handler._user_cachemap[self.u_apple].update(
{"presence": OFFLINE}, serial=0
)
apple_set = self.handler._remote_sendmap.setdefault("apple", set())
apple_set.add(self.u_potato.domain)
yield self.handler.set_state(self.u_apple, self.u_apple,
{"state": ONLINE}
{"presence": ONLINE}
)
yield put_json.await_calls()
@@ -724,54 +749,82 @@ class PresencePushTestCase(unittest.TestCase):
self.room_members = [self.u_banana, self.u_potato]
self.assertEquals(self.event_source.get_current_key(), 0)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence",
content={
"push": [
{"user_id": "@potato:remote",
"state": "online",
"mtime_age": 1000},
"last_active_ago": 1000},
],
}
)
)
self.mock_update_client.assert_has_calls([
call(users_to_push=set([self.u_apple]),
room_ids=["a-room"],
observed_user=self.u_potato,
statuscache=ANY),
], any_order=True)
(events, _) = yield self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)
self.assertEquals(self.event_source.get_current_key(), 1)
self.assertEquals(events,
[
{"type": "m.presence",
"content": {
"user_id": "@potato:remote",
"presence": ONLINE,
"state": ONLINE,
"last_active_ago": 1000,
}}
]
)
self.clock.advance_time(2)
state = yield self.handler.get_state(self.u_potato, self.u_apple)
self.assertEquals({"state": ONLINE, "mtime_age": 3000}, state)
self.assertEquals(
{"state": ONLINE, "presence": ONLINE, "last_active_ago": 3000},
state
)
@defer.inlineCallbacks
def test_join_room_local(self):
self.room_members = [self.u_apple, self.u_banana]
yield self.distributor.fire("user_joined_room", self.u_elderberry,
self.assertEquals(self.event_source.get_current_key(), 0)
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
self.handler._user_cachemap[self.u_clementine].update(
{
"presence": PresenceState.ONLINE,
"last_active": self.clock.time_msec(),
}, self.u_clementine
)
yield self.distributor.fire("user_joined_room", self.u_clementine,
"a-room"
)
self.mock_update_client.assert_has_calls([
call(room_ids=["a-room"],
observed_user=self.u_elderberry,
users_to_push=set(),
statuscache=ANY),
call(users_to_push=set([self.u_elderberry]),
observed_user=self.u_apple,
room_ids=[],
statuscache=ANY),
call(users_to_push=set([self.u_elderberry]),
observed_user=self.u_banana,
room_ids=[],
statuscache=ANY),
], any_order=True)
(events, _) = yield self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)
self.assertEquals(self.event_source.get_current_key(), 1)
self.assertEquals(events,
[
{"type": "m.presence",
"content": {
"user_id": "@clementine:test",
"presence": ONLINE,
"state": ONLINE,
"last_active_ago": 0,
}}
]
)
@defer.inlineCallbacks
def test_join_room_remote(self):
@@ -784,7 +837,8 @@ class PresencePushTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@apple:test",
"state": "online"},
"presence": "online",
"state": "online"},
],
}
),
@@ -798,7 +852,8 @@ class PresencePushTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@banana:test",
"state": "offline"},
"presence": "offline",
"state": "offline"},
],
}
),
@@ -809,7 +864,7 @@ class PresencePushTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
self.handler._user_cachemap[self.u_apple].update(
{"state": PresenceState.ONLINE}, self.u_apple)
{"presence": PresenceState.ONLINE}, self.u_apple)
self.room_members = [self.u_apple, self.u_banana]
yield self.distributor.fire("user_joined_room", self.u_potato,
@@ -822,12 +877,13 @@ class PresencePushTestCase(unittest.TestCase):
put_json.expect_call_and_return(
call("remote",
path="/matrix/federation/v1/send/1000002/",
path="/_matrix/federation/v1/send/1000002/",
data=_expect_edu("remote", "m.presence",
content={
"push": [
{"user_id": "@clementine:test",
"state": "online"},
"presence": "online",
"state": "online"},
],
}
),
@@ -837,7 +893,7 @@ class PresencePushTestCase(unittest.TestCase):
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
self.handler._user_cachemap[self.u_clementine].update(
{"state": ONLINE}, self.u_clementine)
{"presence": ONLINE}, self.u_clementine)
self.room_members.append(self.u_potato)
yield self.distributor.fire("user_joined_room", self.u_clementine,
@@ -921,7 +977,8 @@ class PresencePollingTestCase(unittest.TestCase):
def get_presence_state(user_localpart):
return defer.succeed(
{"state": self.current_user_state[user_localpart],
"status_msg": None}
"status_msg": None,
"mtime": 123456000}
)
self.datastore.get_presence_state = get_presence_state
@@ -955,7 +1012,7 @@ class PresencePollingTestCase(unittest.TestCase):
# apple goes online
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"state": ONLINE}
state={"presence": ONLINE}
)
# apple should see both banana and clementine currently offline
@@ -978,8 +1035,9 @@ class PresencePollingTestCase(unittest.TestCase):
# banana goes online
yield self.handler.set_state(
target_user=self.u_banana, auth_user=self.u_banana,
state={"state": ONLINE})
target_user=self.u_banana, auth_user=self.u_banana,
state={"presence": ONLINE}
)
# apple and banana should now both see each other online
self.mock_update_client.assert_has_calls([
@@ -999,8 +1057,9 @@ class PresencePollingTestCase(unittest.TestCase):
# apple goes offline
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"state": OFFLINE})
target_user=self.u_apple, auth_user=self.u_apple,
state={"presence": OFFLINE}
)
# banana should now be told apple is offline
self.mock_update_client.assert_has_calls([
@@ -1013,7 +1072,6 @@ class PresencePollingTestCase(unittest.TestCase):
self.assertFalse("banana" in self.handler._local_pushmap)
self.assertFalse("clementine" in self.handler._local_pushmap)
@defer.inlineCallbacks
def test_remote_poll_send(self):
put_json = self.mock_http_client.put_json
@@ -1043,8 +1101,9 @@ class PresencePollingTestCase(unittest.TestCase):
# clementine goes online
yield self.handler.set_state(
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"state": ONLINE})
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"presence": ONLINE}
)
yield put_json.await_calls()
@@ -1071,7 +1130,7 @@ class PresencePollingTestCase(unittest.TestCase):
# 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}
state={"presence": ONLINE}
)
# reactor.iterate(delay=0)
@@ -1081,7 +1140,7 @@ class PresencePollingTestCase(unittest.TestCase):
# fig goes offline
yield self.handler.set_state(
target_user=self.u_fig, auth_user=self.u_fig,
state={"state": OFFLINE}
state={"presence": OFFLINE}
)
reactor.iterate(delay=0)
@@ -1102,8 +1161,9 @@ class PresencePollingTestCase(unittest.TestCase):
# clementine goes offline
yield self.handler.set_state(
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"state": OFFLINE})
target_user=self.u_clementine, auth_user=self.u_clementine,
state={"presence": OFFLINE}
)
yield put_json.await_calls()
@@ -1116,11 +1176,12 @@ 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="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("remote", "m.presence",
content={
"push": [
{"user_id": "@banana:test",
"presence": "offline",
"state": "offline",
"status_msg": None},
],
@@ -1131,7 +1192,7 @@ class PresencePollingTestCase(unittest.TestCase):
)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("remote", "m.presence",
content={
"poll": [ "@banana:test" ],
@@ -1145,7 +1206,7 @@ class PresencePollingTestCase(unittest.TestCase):
self.assertTrue(self.u_banana in self.handler._remote_sendmap)
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000001/",
"/_matrix/federation/v1/send/1000001/",
_make_edu_json("remote", "m.presence",
content={
"unpoll": [ "@banana:test" ],

View File

@@ -166,7 +166,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
from synapse.handlers.presence import UserPresenceCache
self.handlers.presence_handler._user_cachemap[self.u_apple] = (
UserPresenceCache())
UserPresenceCache()
)
self.handlers.presence_handler._user_cachemap[self.u_apple].update(
{"presence": OFFLINE}, serial=0
)
apple_set = self.handlers.presence_handler._local_pushmap.setdefault(
"apple", set())
apple_set.add(self.u_banana)
@@ -182,11 +186,13 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
self.assertEquals([
{"observed_user": self.u_banana,
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 0,
"last_active_ago": 0,
"displayname": "Frank",
"avatar_url": "http://foo"},
{"observed_user": self.u_clementine,
"presence": OFFLINE,
"state": OFFLINE}],
presence)
@@ -199,8 +205,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
statuscache = self.mock_update_client.call_args[1]["statuscache"]
self.assertEquals({
"state": ONLINE,
"mtime": 1000000, # MockClock
"presence": ONLINE,
"last_active": 1000000, # MockClock
"displayname": "Frank",
"avatar_url": "http://foo",
}, statuscache.state)
@@ -222,8 +228,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
statuscache = self.mock_update_client.call_args[1]["statuscache"]
self.assertEquals({
"state": ONLINE,
"mtime": 1000000, # MockClock
"presence": ONLINE,
"last_active": 1000000, # MockClock
"displayname": "I am an Apple",
"avatar_url": "http://foo",
}, statuscache.state)
@@ -241,7 +247,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
# TODO(paul): Gut-wrenching
from synapse.handlers.presence import UserPresenceCache
self.handlers.presence_handler._user_cachemap[self.u_apple] = (
UserPresenceCache())
UserPresenceCache()
)
self.handlers.presence_handler._user_cachemap[self.u_apple].update(
{"presence": OFFLINE}, serial=0
)
apple_set = self.handlers.presence_handler._remote_sendmap.setdefault(
"apple", set())
apple_set.add(self.u_potato.domain)
@@ -255,8 +265,9 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
content={
"push": [
{"user_id": "@apple:test",
"presence": "online",
"state": "online",
"mtime_age": 0,
"last_active_ago": 0,
"displayname": "Frank",
"avatar_url": "http://foo"},
],
@@ -293,14 +304,16 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
statuscache=ANY)
statuscache = self.mock_update_client.call_args[1]["statuscache"]
self.assertEquals({"state": ONLINE,
self.assertEquals({"presence": ONLINE,
"displayname": "Frank",
"avatar_url": "http://foo"}, statuscache.state)
state = yield self.handlers.presence_handler.get_state(self.u_potato,
self.u_apple)
self.assertEquals({"state": ONLINE,
"displayname": "Frank",
"avatar_url": "http://foo"},
state)
self.assertEquals(
{"presence": ONLINE,
"state": ONLINE,
"displayname": "Frank",
"avatar_url": "http://foo"},
state)

View File

@@ -330,6 +330,8 @@ class RoomCreationTest(unittest.TestCase):
datastore=NonCallableMock(spec_set=[
"store_room",
"snapshot_room",
"persist_event",
"get_joined_hosts_for_room",
]),
http_client=NonCallableMock(spec_set=[]),
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
@@ -362,6 +364,10 @@ class RoomCreationTest(unittest.TestCase):
])
self.room_member_handler = self.handlers.room_member_handler
def hosts(room):
return defer.succeed([])
self.datastore.get_joined_hosts_for_room.side_effect = hosts
@defer.inlineCallbacks
def test_room_creation(self):
user_id = "@foo:red"
@@ -385,9 +391,3 @@ class RoomCreationTest(unittest.TestCase):
self.assertTrue(self.state_handler.handle_new_event.called)
self.assertTrue(self.federation.handle_new_event.called)
config_event = self.federation.handle_new_event.call_args[0][0]
self.assertEquals(RoomConfigEvent.TYPE, config_event.type)
self.assertEquals(room_id, config_event.room_id)
self.assertEquals(user_id, config_event.user_id)
self.assertEquals(config, config_event.content)

View File

@@ -166,7 +166,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("farm",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("farm", "m.typing",
content={
"room_id": self.room_id,
@@ -192,7 +192,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
self.room_members = [self.u_apple, self.u_onion]
yield self.mock_federation_resource.trigger("PUT",
"/matrix/federation/v1/send/1000000/",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("farm", "m.typing",
content={
"room_id": self.room_id,
@@ -216,7 +216,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
put_json = self.mock_http_client.put_json
put_json.expect_call_and_return(
call("farm",
path="/matrix/federation/v1/send/1000000/",
path="/_matrix/federation/v1/send/1000000/",
data=_expect_edu("farm", "m.typing",
content={
"room_id": self.room_id,

View File

@@ -36,7 +36,7 @@ from mock import Mock
logging.getLogger().addHandler(logging.NullHandler())
PATH_PREFIX = "/matrix/client/api/v1"
PATH_PREFIX = "/_matrix/client/api/v1"
class EventStreamPaginationApiTestCase(unittest.TestCase):

View File

@@ -37,7 +37,7 @@ ONLINE = PresenceState.ONLINE
myid = "@apple:test"
PATH_PREFIX = "/matrix/client/api/v1"
PATH_PREFIX = "/_matrix/client/api/v1"
class JustPresenceHandlers(object):
@@ -98,8 +98,10 @@ class PresenceStateTestCase(unittest.TestCase):
"/presence/%s/status" % (myid), None)
self.assertEquals(200, code)
self.assertEquals({"state": ONLINE, "status_msg": "Available"},
response)
self.assertEquals(
{"presence": ONLINE, "state": ONLINE, "status_msg": "Available"},
response
)
mocked_get.assert_called_with("apple")
@defer.inlineCallbacks
@@ -109,7 +111,7 @@ class PresenceStateTestCase(unittest.TestCase):
(code, response) = yield self.mock_resource.trigger("PUT",
"/presence/%s/status" % (myid),
'{"state": "unavailable", "status_msg": "Away"}')
'{"presence": "unavailable", "status_msg": "Away"}')
self.assertEquals(200, code)
mocked_set.assert_called_with("apple",
@@ -173,9 +175,9 @@ class PresenceListTestCase(unittest.TestCase):
"/presence/list/%s" % (myid), None)
self.assertEquals(200, code)
self.assertEquals(
[{"user_id": "@banana:test", "state": OFFLINE}], response
)
self.assertEquals([
{"user_id": "@banana:test", "presence": OFFLINE, "state": OFFLINE},
], response)
self.datastore.get_presence_list.assert_called_with(
"apple", accepted=True
@@ -229,7 +231,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
# HIDEOUS HACKERY
# TODO(paul): This should be injected in via the HomeServer DI system
from synapse.streams.events import (
PresenceSource, NullSource, EventSources
PresenceEventSource, NullSource, EventSources
)
old_SOURCE_TYPES = EventSources.SOURCE_TYPES
@@ -240,7 +242,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
EventSources.SOURCE_TYPES = {
k: NullSource for k in old_SOURCE_TYPES.keys()
}
EventSources.SOURCE_TYPES["presence"] = PresenceSource
EventSources.SOURCE_TYPES["presence"] = PresenceEventSource
hs = HomeServer("test",
db_pool=None,
@@ -274,6 +276,15 @@ class PresenceEventStreamTestCase(unittest.TestCase):
lambda u: defer.succeed([]))
self.mock_datastore = hs.get_datastore()
def get_profile_displayname(user_id):
return defer.succeed("Frank")
self.mock_datastore.get_profile_displayname = get_profile_displayname
def get_profile_avatar_url(user_id):
return defer.succeed(None)
self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url
self.presence = hs.get_handlers().presence_handler
self.u_apple = hs.parse_userid("@apple:test")
@@ -295,7 +306,9 @@ class PresenceEventStreamTestCase(unittest.TestCase):
# all be ours
# I'll already get my own presence state change
self.assertEquals({"start": "0_1", "end": "0_1", "chunk": []}, response)
self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []},
response
)
self.mock_datastore.set_presence_state.return_value = defer.succeed(
{"state": ONLINE})
@@ -303,17 +316,20 @@ class PresenceEventStreamTestCase(unittest.TestCase):
[])
yield self.presence.set_state(self.u_banana, self.u_banana,
state={"state": ONLINE})
state={"presence": ONLINE}
)
(code, response) = yield self.mock_resource.trigger("GET",
"/events?from=0_1&timeout=0", None)
"/events?from=0_1_0&timeout=0", None)
self.assertEquals(200, code)
self.assertEquals({"start": "0_1", "end": "0_2", "chunk": [
self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [
{"type": "m.presence",
"content": {
"user_id": "@banana:test",
"presence": ONLINE,
"state": ONLINE,
"mtime_age": 0,
"displayname": "Frank",
"last_active_ago": 0,
}},
]}, response)

View File

@@ -26,7 +26,7 @@ from synapse.api.errors import SynapseError, AuthError
from synapse.server import HomeServer
myid = "@1234ABCD:test"
PATH_PREFIX = "/matrix/client/api/v1"
PATH_PREFIX = "/_matrix/client/api/v1"
class ProfileTestCase(unittest.TestCase):
""" Tests profile management. """

View File

@@ -32,7 +32,7 @@ from .utils import RestTestCase
from mock import Mock
PATH_PREFIX = "/matrix/client/api/v1"
PATH_PREFIX = "/_matrix/client/api/v1"
class RoomPermissionsTestCase(RestTestCase):
@@ -51,7 +51,7 @@ class RoomPermissionsTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@@ -398,7 +398,7 @@ class RoomsMemberListTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@@ -476,7 +476,7 @@ class RoomsCreateTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@@ -566,7 +566,7 @@ class RoomTopicTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@@ -669,7 +669,7 @@ class RoomMemberStateTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),
@@ -794,7 +794,7 @@ class RoomMessagesTestCase(RestTestCase):
persistence_service.get_latest_pdus_in_context.return_value = []
hs = HomeServer(
"test",
"red",
db_pool=None,
http_client=None,
datastore=MemoryDataStore(),

View File

@@ -188,8 +188,9 @@ class MemoryDataStore(object):
def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
return [
r for r in self.members
if self.members[r].get(user_id).membership in membership_list
self.members[r].get(user_id) for r in self.members
if user_id in self.members[r] and
self.members[r][user_id].membership in membership_list
]
def get_room_events_stream(self, user_id=None, from_key=None, to_key=None,
@@ -233,6 +234,23 @@ class MemoryDataStore(object):
def get_room_events_max_id(self):
return 0 # TODO (erikj)
def get_send_event_level(self, room_id):
return defer.succeed(0)
def get_power_level(self, room_id, user_id):
return defer.succeed(0)
def get_add_state_level(self, room_id):
return defer.succeed(0)
def get_room_join_rule(self, room_id):
# TODO (erikj): This should be configurable
return defer.succeed("invite")
def get_ops_levels(self, room_id):
return defer.succeed((5, 5))
def _format_call(args, kwargs):
return ", ".join(
["%r" % (a) for a in args] +

View File

@@ -21,8 +21,8 @@ limitations under the License.
'use strict';
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService',
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) {
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService',
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService, matrixPhoneService) {
// Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path();
@@ -36,6 +36,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
eventStreamService.resume();
mPresence.start();
}
$scope.user_id;
var config = matrixService.config();
if (config) {
$scope.user_id = matrixService.config().user_id;
}
/**
* Open a given page.
@@ -45,6 +51,16 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
$location.url(url);
};
// Open the given user profile page
$scope.goToUserPage = function(user_id) {
if (user_id === $scope.user_id) {
$location.url("/settings");
}
else {
$location.url("/user/" + user_id);
}
};
// Logs the user out
$scope.logout = function() {
@@ -69,13 +85,30 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
$scope.logout();
});
$scope.requestNotifications = function() {
if (window.Notification) {
console.log("Notification.permission: " + window.Notification.permission);
window.Notification.requestPermission(function(){});
}
$scope.updateHeader = function() {
$scope.user_id = matrixService.config().user_id;
};
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
console.trace("incoming call");
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
$rootScope.currentCall = call;
});
$scope.answerCall = function() {
$scope.currentCall.answer();
};
$scope.hangupCall = function() {
$scope.currentCall.hangup();
$scope.currentCall = undefined;
};
}]);
$rootScope.onCallError = function(errStr) {
$scope.feedback = errStr;
}
$rootScope.onCallHangup = function() {
}
}]);

View File

@@ -32,7 +32,12 @@ angular.module('matrixWebClient')
.directive('ngFocus', ['$timeout', function($timeout) {
return {
link: function(scope, element, attr) {
$timeout(function() { element[0].focus(); }, 0);
// XXX: slightly evil hack to disable autofocus on iOS, as in general
// it causes more problems than it fixes, by bouncing the page
// around
if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) {
$timeout(function() { element[0].focus(); }, 0);
}
}
};
}]);

View File

@@ -70,7 +70,7 @@ angular.module('matrixWebClient')
});
filtered.sort(function (a, b) {
return ((a["mtime_age"] || 10e10) > (b["mtime_age"] || 10e10) ? 1 : -1);
return ((a["last_active_ago"] || 10e10) > (b["last_active_ago"] || 10e10) ? 1 : -1);
});
return filtered;
};
@@ -79,4 +79,43 @@ angular.module('matrixWebClient')
return function(text) {
return $sce.trustAsHtml(text);
};
}])
// Compute the room name according to information we have
.filter('roomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) {
return function(room_id) {
var roomName;
// If there is an alias, use it
// TODO: only one alias is managed for now
var alias = matrixService.getRoomIdToAliasMapping(room_id);
if (alias) {
roomName = alias;
}
if (undefined === roomName) {
// Else, build the name from its users
var room = $rootScope.events.rooms[room_id];
if (room) {
if (room.members) {
// Limit the room renaming to 1:1 room
if (2 === Object.keys(room.members).length) {
for (var i in room.members) {
var member = room.members[i];
if (member.user_id !== matrixService.config().user_id) {
roomName = member.content.displayname ? member.content.displayname : member.user_id;
}
}
}
}
}
}
if (undefined === roomName) {
// By default, use the room ID
roomName = room_id;
}
return roomName;
};
}]);

334
webclient/app.css Normal file → Executable file
View File

@@ -1,121 +1,198 @@
/*** 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;
}
/** Common layout **/
html {
height: 100%;
}
body {
height: 100%;
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 0px;
}
h1 {
font-family: Helvetica, Arial, sans-serif;
font-size: 20pt;
}
/*** Overall page layout ***/
a:link { color: #666; }
a:visited { color: #666; }
a:hover { color: #000; }
a:active { color: #000; }
#page {
position: absolute;
top: 80px;
bottom: 100px;
left: 0px;
right: 0px;
margin: 20px;
min-height: 100%;
margin-bottom: -32px; /* to make room for the footer */
}
#wrapper {
margin: auto;
max-width: 1280px;
padding-top: 40px;
padding-bottom: 40px;
padding-left: 20px;
padding-right: 20px;
}
#header
{
position: absolute;
top: 0px;
width: 100%;
background-color: #333;
height: 32px;
}
#callBar {
float: left;
}
#headerContent {
color: #ccc;
max-width: 1280px;
margin: auto;
text-align: right;
height: 32px;
line-height: 32px;
}
#headerContent a:link,
#headerContent a:visited,
#headerContent a:hover,
#headerContent a:active {
color: #fff;
}
#footer
{
width: 100%;
border-top: #666 1px solid;
background-color: #aaa;
height: 32px;
}
#footerContent
{
font-size: 8pt;
color: #fff;
max-width: 1280px;
margin: auto;
text-align: center;
height: 32px;
line-height: 32px;
}
#genericHeading
{
margin-top: 13px;
}
#feedback {
color: #800;
}
.mouse-pointer {
cursor: pointer;
}
.invited {
opacity: 0.2;
}
/*** Login Pages ***/
.loginWrapper {
text-align: center;
}
#loginForm {
text-align: left;
padding: 1em;
margin-bottom: 40px;
display: inline-block;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
#loginForm input[type='radio'] {
margin-right: 1em;
}
#serverConfig {
text-align: center;
}
#serverConfig,
#serverConfig input,
#serverConfig button
{
font-size: 10pt ! important;
}
.smallPrint {
color: #888;
font-size: 9pt ! important;
font-style: italic ! important;
}
#serverConfig label {
display: inline-block;
text-align: right;
margin-right: 0.5em;
width: 7em;
}
#loginForm,
#loginForm input,
#loginForm button,
#loginForm select {
font-size: 18px;
}
/*** Room page ***/
#roomPage {
position: absolute;
top: 120px;
bottom: 120px;
left: 20px;
right: 20px;
}
#roomWrapper {
margin: auto;
max-width: 1280px;
height: 100%;
}
#roomName {
max-width: 1280px;
width: 100%;
text-align: right;
top: -40px;
position: absolute;
float: right;
font-size: 16px;
margin-top: 15px;
}
#roomHeader {
margin: auto;
padding-left: 20px;
padding-right: 20px;
padding-top: 53px;
max-width: 1280px;
}
#controlPanel {
position: absolute;
bottom: 0px;
width: 100%;
height: 100px;
background-color: #f8f8f8;
border-top: #aaa 1px solid;
}
@@ -146,10 +223,6 @@ h1 {
background-color: #faa;
}
.mouse-pointer {
cursor: pointer;
}
/*** Participant list ***/
#usersTableWrapper {
@@ -195,6 +268,13 @@ h1 {
word-break: break-all;
}
.userAvatar .userPowerLevel {
position: absolute;
bottom: 20px;
height: 1px;
background-color: red;
}
.userPresence {
text-align: center;
font-size: 12px;
@@ -300,7 +380,7 @@ h1 {
display: inline-block;
margin-bottom: -1px;
max-width: 90%;
font-size: 16px;
font-size: 14px;
word-wrap: break-word;
padding-top: 7px;
padding-bottom: 5px;
@@ -310,6 +390,11 @@ h1 {
-webkit-text-size-adjust:100%
}
.bubble img {
max-width: 100%;
max-height: auto;
}
.differentUser td {
padding-bottom: 5px ! important;
}
@@ -341,8 +426,8 @@ h1 {
}
#room-fullscreen-image img {
max-width: 100%;
max-height: 100%;
max-width: 90%;
max-height: 90%;
bottom: 0;
left: 0;
margin: auto;
@@ -350,9 +435,14 @@ h1 {
position: fixed;
right: 0;
top: 0;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
}
/*** Recents ***/
.recentsTable {
max-width: 480px;
width: 100%;
@@ -402,11 +492,14 @@ h1 {
}
/*** Recents in the room page ***/
#roomRecentsTableWrapper {
float: left;
max-width: 320px;
margin-right: 20px;
padding-right: 10px;
margin-right: 10px;
height: 100%;
border-right: 1px solid #ddd;
overflow-y: auto;
}
@@ -421,55 +514,14 @@ h1 {
}
.profile-avatar img {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
object-fit: cover;
}
/*** User profile page ***/
#user-ids {
padding-left: 1em;
}
#user-displayname {
font-size: 24px;
}
/******************************/
#header
{
padding: 20px;
max-width: 1280px;
margin: auto;
}
#logo,
#roomLogo {
max-width: 1280px;
margin: auto;
}
#header-buttons {
float: right;
}
.text_entry_section {
position: fixed;
bottom: 0;
z-index: 100;
left: 0;
right: 10em;
width: 100%;
background: #e0e0e0;
}
.member_invited {
color: blue;
}
.member_joined {
}
.member_left {
color: gray;
}

View File

@@ -18,6 +18,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
'ngRoute',
'MatrixWebClientController',
'LoginController',
'RegisterController',
'RoomController',
'HomeController',
'RecentsController',
@@ -35,30 +36,27 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
function($routeProvider, $provide, $httpProvider) {
$routeProvider.
when('/login', {
templateUrl: 'login/login.html',
controller: 'LoginController'
templateUrl: 'login/login.html'
}).
when('/register', {
templateUrl: 'login/register.html'
}).
when('/room/:room_id_or_alias', {
templateUrl: 'room/room.html',
controller: 'RoomController'
templateUrl: 'room/room.html'
}).
when('/room/', { // room URL with room alias in it (ex: http://127.0.0.1:8000/#/room/#public:localhost:8080) will come here.
// The reason is that 2nd hash key breaks routeProvider parameters cutting so that the URL will not match with
// the previous '/room/:room_id_or_alias' URL rule
templateUrl: 'room/room.html',
controller: 'RoomController'
templateUrl: 'room/room.html'
}).
when('/', {
templateUrl: 'home/home.html',
controller: 'HomeController'
templateUrl: 'home/home.html'
}).
when('/settings', {
templateUrl: 'settings/settings.html',
controller: 'SettingsController'
templateUrl: 'settings/settings.html'
}).
when('/user/:user_matrix_id', {
templateUrl: 'user/user.html',
controller: 'UserController'
templateUrl: 'user/user.html'
}).
otherwise({
redirectTo: '/'
@@ -84,7 +82,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) {
// If user auth details are not in cache, go to the login page
if (!matrixService.isUserLoggedIn()) {
if (!matrixService.isUserLoggedIn() &&
$location.path() !== "/login" &&
$location.path() !== "/register")
{
$location.path("login");
}

View File

@@ -30,7 +30,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
*/
this.uploadFile = function(file) {
var deferred = $q.defer();
console.log("Uploading " + file.name + "... to /matrix/content");
console.log("Uploading " + file.name + "... to /_matrix/content");
matrixService.uploadContent(file).then(
function(response) {
var content_url = response.data.content_token;

View File

@@ -28,6 +28,7 @@ if typically all the $on method would do is update its own $scope.
*/
angular.module('eventHandlerService', [])
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
var MSG_EVENT = "MSG_EVENT";
var MEMBER_EVENT = "MEMBER_EVENT";
var PRESENCE_EVENT = "PRESENCE_EVENT";
@@ -48,7 +49,7 @@ angular.module('eventHandlerService', [])
$rootScope.events.rooms[room_id].messages = [];
$rootScope.events.rooms[room_id].members = {};
}
}
};
var resetRoomMessages = function(room_id) {
if ($rootScope.events.rooms[room_id]) {
@@ -56,6 +57,13 @@ angular.module('eventHandlerService', [])
}
};
var handleRoomCreate = function(event, isLiveEvent) {
initRoom(event.room_id);
// For now, we do not use the event data. Simply signal it to the app controllers
$rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
};
var handleMessage = function(event, isLiveEvent) {
initRoom(event.room_id);
@@ -95,12 +103,22 @@ angular.module('eventHandlerService', [])
$rootScope.presence[event.content.user_id] = event;
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
};
var handlePowerLevels = function(event, isLiveEvent) {
initRoom(event.room_id);
$rootScope.events.rooms[event.room_id][event.type] = event;
//TODO
//$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
};
var handleCallEvent = function(event, isLiveEvent) {
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
};
return {
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
MSG_EVENT: MSG_EVENT,
MEMBER_EVENT: MEMBER_EVENT,
PRESENCE_EVENT: PRESENCE_EVENT,
@@ -109,6 +127,9 @@ angular.module('eventHandlerService', [])
handleEvent: function(event, isLiveEvent) {
switch(event.type) {
case "m.room.create":
handleRoomCreate(event, isLiveEvent);
break;
case "m.room.message":
handleMessage(event, isLiveEvent);
break;
@@ -118,11 +139,20 @@ angular.module('eventHandlerService', [])
case "m.presence":
handlePresence(event, isLiveEvent);
break;
case 'm.room.ops_levels':
case 'm.room.send_event_level':
case 'm.room.add_state_level':
case 'm.room.join_rules':
case 'm.room.power_levels':
handlePowerLevels(event, isLiveEvent);
break;
default:
console.log("Unable to handle event type " + event.type);
console.log(JSON.stringify(event, undefined, 4));
break;
}
if (event.type.indexOf('m.call.') == 0) {
if (event.type.indexOf('m.call.') === 0) {
handleCallEvent(event, isLiveEvent);
}
},

View File

@@ -36,7 +36,7 @@ var forAllTracksOnStream = function(s, f) {
}
angular.module('MatrixCall', [])
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) {
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope) {
var MatrixCall = function(room_id) {
this.room_id = room_id;
this.call_id = "c" + new Date().getTime();
@@ -73,9 +73,7 @@ angular.module('MatrixCall', [])
this.state = 'wait_local_media';
};
MatrixCall.prototype.hangup = function() {
console.trace("Ending call "+this.call_id);
MatrixCall.prototype.stopAllMedia = function() {
if (this.localAVStream) {
forAllTracksOnStream(this.localAVStream, function(t) {
t.stop();
@@ -86,6 +84,12 @@ angular.module('MatrixCall', [])
t.stop();
});
}
};
MatrixCall.prototype.hangup = function() {
console.trace("Ending call "+this.call_id);
this.stopAllMedia();
var content = {
version: 0,
@@ -204,6 +208,7 @@ angular.module('MatrixCall', [])
// 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';
$rootScope.$apply();
}
};
@@ -232,8 +237,9 @@ angular.module('MatrixCall', [])
t.onstarted = self.onRemoteStreamTrackStarted;
});
event.stream.onended = function(e) { self.onRemoteStreamEnded(e); };
// not currently implemented in chrome
event.stream.onstarted = this.onRemoteStreamStarted;
event.stream.onstarted = function(e) { self.onRemoteStreamStarted(e); };
var player = new Audio();
player.src = URL.createObjectURL(s);
player.play();
@@ -243,24 +249,19 @@ angular.module('MatrixCall', [])
this.state = 'connected';
};
MatrixCall.prototype.onRemoteStreamEnded = function(event) {
this.state = 'ended';
this.stopAllMedia();
this.onHangup();
};
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.stopAllMedia();
this.onHangup();
};

View File

@@ -38,7 +38,7 @@ angular.module('matrixService', [])
// Current version of permanent storage
var configVersion = 0;
var prefixPath = "/matrix/client/api/v1";
var prefixPath = "/_matrix/client/api/v1";
var MAPPING_PREFIX = "alias_for_";
var doRequest = function(method, path, params, data, $httpParams) {
@@ -95,14 +95,18 @@ angular.module('matrixService', [])
},
// Create a room
create: function(room_id, visibility) {
create: function(room_alias, visibility) {
// The REST path spec
var path = "/createRoom";
return doRequest("POST", path, undefined, {
visibility: visibility,
room_alias_name: room_id
});
var req = {
"visibility": visibility
};
if (room_alias) {
req.room_alias_name = room_alias;
}
return doRequest("POST", path, undefined, req);
},
// List all rooms joined or been invited to
@@ -164,7 +168,7 @@ angular.module('matrixService', [])
// Retrieves the room ID corresponding to a room alias
resolveRoomAlias:function(room_alias) {
var path = "/matrix/client/api/v1/directory/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);
@@ -304,7 +308,7 @@ angular.module('matrixService', [])
// hit the Identity Server for a 3PID request.
linkEmail: function(email, clientSecret, sendAttempt) {
var path = "/matrix/identity/api/v1/validate/email/requestToken"
var path = "/_matrix/identity/api/v1/validate/email/requestToken"
var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
var headers = {};
headers["Content-Type"] = "application/x-www-form-urlencoded";
@@ -312,7 +316,7 @@ angular.module('matrixService', [])
},
authEmail: function(clientSecret, tokenId, code) {
var path = "/matrix/identity/api/v1/validate/email/submitToken";
var path = "/_matrix/identity/api/v1/validate/email/submitToken";
var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
var headers = {};
headers["Content-Type"] = "application/x-www-form-urlencoded";
@@ -320,7 +324,7 @@ angular.module('matrixService', [])
},
bindEmail: function(userId, tokenId, clientSecret) {
var path = "/matrix/identity/api/v1/3pid/bind";
var path = "/_matrix/identity/api/v1/3pid/bind";
var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
var headers = {};
headers["Content-Type"] = "application/x-www-form-urlencoded";
@@ -328,7 +332,7 @@ angular.module('matrixService', [])
},
uploadContent: function(file) {
var path = "/matrix/content";
var path = "/_matrix/content";
var headers = {
"Content-Type": undefined // undefined means angular will figure it out
};
@@ -480,6 +484,30 @@ angular.module('matrixService', [])
getRoomIdToAliasMapping: function(roomId) {
return localStorage.getItem(MAPPING_PREFIX+roomId);
},
/****** Power levels management ******/
/**
* Return the power level of an user in a particular room
* @param {String} room_id the room id
* @param {String} user_id the user id
* @returns {Number} a value between 0 and 10
*/
getUserPowerLevel: function(room_id, user_id) {
var powerLevel = 0;
var room = $rootScope.events.rooms[room_id];
if (room && room["m.room.power_levels"]) {
if (user_id in room["m.room.power_levels"].content) {
powerLevel = room["m.room.power_levels"].content[user_id];
}
else {
// Use the room default user power
powerLevel = room["m.room.power_levels"].content["default"];
}
}
return powerLevel;
}
};

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