diff options
41 files changed, 2144 insertions, 347 deletions
diff --git a/README.rst b/README.rst index 2d355d9649..98af91ea42 100644 --- a/README.rst +++ b/README.rst @@ -2,11 +2,11 @@ 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: +VoIP. The basics you need to know to get up and running are: - Chatrooms are distributed and do not exist on any single server. Rooms can be found using names like ``#matrix:matrix.org`` or - ``#test:localhost:8080`` or they can be ephemeral. + ``#test:localhost:8008`` or they can be ephemeral. - Matrix user IDs look like ``@matthew:matrix.org`` (although in the future you will normally refer to yourself and others using a 3PID: email @@ -14,8 +14,8 @@ VoIP[1]. The basics you need to know to get up and running are: The overall architecture is:: - client <----> homeserver <=================> homeserver <-----> client - e.g. matrix.org:8080 e.g. mydomain.net:8080 + client <----> homeserver <=====================> homeserver <----> client + https://matrix.org/_matrix https://mydomain.net/_matrix Quick Start =========== @@ -25,22 +25,20 @@ To get up and running: - To simply play with an **existing** homeserver you can just go straight to http://matrix.org/alpha. - - To run your own **private** homeserver on localhost:8080, install synapse + - To run your own **private** homeserver on localhost:8008, install synapse with ``python setup.py develop --user`` and then run one with ``python synapse/app/homeserver.py`` - you will find a webclient running - at http://localhost:8080 (use a recent Chrome, Safari or Firefox for now, + at http://localhost:8008 (use a recent Chrome, Safari or Firefox for now, please...) - To make the homeserver **public** and let it exchange messages with other homeservers and participate in the overall Matrix federation, open - up port 8080 and run ``python synapse/app/homeserver.py --host + up port 8448 and run ``python synapse/app/homeserver.py --host machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and say hi! :) For more detailed setup instructions, please see further down this document. -[1] VoIP currently in development - About Matrix ============ @@ -50,15 +48,15 @@ which handle: - Creating and managing fully distributed chat rooms with no single points of control or failure - - Eventually-consistent cryptographically secure[2] synchronisation of room + - Eventually-consistent cryptographically secure[1] synchronisation of room state across a global open network of federated servers and services - Sending and receiving extensible messages in a room with (optional) - end-to-end encryption[3] + end-to-end encryption[2] - Inviting, joining, leaving, kicking, banning room members - Managing user accounts (registration, login, logout) - Using 3rd Party IDs (3PIDs) such as email addresses, phone numbers, Facebook accounts to authenticate, identify and discover users on Matrix. - - Placing 1:1 VoIP and Video calls (in development) + - Placing 1:1 VoIP and Video calls These APIs are intended to be implemented on a wide range of servers, services and clients, letting developers build messaging and VoIP functionality on top of @@ -92,9 +90,9 @@ https://github.com/matrix-org/synapse/issues or at matrix@matrix.org. Thanks for trying Matrix! -[2] Cryptographic signing of messages isn't turned on yet +[1] Cryptographic signing of messages isn't turned on yet -[3] End-to-end encryption is currently in development +[2] End-to-end encryption is currently in development Homeserver Installation diff --git a/cmdclient/console.py b/cmdclient/console.py index 7678b5e352..f6a7731eab 100755 --- a/cmdclient/console.py +++ b/cmdclient/console.py @@ -194,7 +194,7 @@ class SynapseCmd(cmd.Cmd): user = "@" + user + ":" + self._domain() reactor.callFromThread(self._do_login, user, p) - print " got %s " % p + #print " got %s " % p except Exception as e: print e diff --git a/docs/client-server/howto.rst b/docs/client-server/howto.rst index 3660c73d36..c02ea8d897 100644 --- a/docs/client-server/howto.rst +++ b/docs/client-server/howto.rst @@ -1,9 +1,8 @@ -TODO(kegan): Tweak joinalias API keys/path? Event stream historical > live needs -a token (currently doesn't). im/sync responses include outdated event formats -(room membership change messages). Room config (specifically: message history, -public rooms). /register seems super simplistic compared to /login, maybe it -would be better if /register used the same technique as /login? /register should -be "user" not "user_id". +.. TODO kegan + Room config (specifically: message history, + public rooms). /register seems super simplistic compared to /login, maybe it + would be better if /register used the same technique as /login? /register should + be "user" not "user_id". How to use the client-server API @@ -15,7 +14,7 @@ implementation, there may be variations in relation to registering/logging in which are not covered in extensive detail in this guide. If you haven't already, get a home server up and running on -``http://localhost:8080``. +``http://localhost:8008``. Accounts @@ -23,14 +22,16 @@ Accounts Before you can send and receive messages, you must **register** for an account. If you already have an account, you must **login** into it. -**Try out the fiddle: http://jsfiddle.net/jrf1h02d/** +`Try out the fiddle`__ + +.. __: http://jsfiddle.net/4q2jyxng/ 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:8008/_matrix/client/api/v1/register" { "access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc", @@ -51,13 +52,17 @@ 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:8008/_matrix/client/api/v1/login" { - "type": "m.login.password" + "flows": [ + { + "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:8008/_matrix/client/api/v1/login" { "access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd", @@ -80,14 +85,16 @@ Communicating In order to communicate with another user, you must **create a room** with that user and **send a message** to that room. -**Try out the fiddle: http://jsfiddle.net/jnwqcshc/** +`Try out the fiddle`__ + +.. __: http://jsfiddle.net/zL3zto9g/ 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:8008/_matrix/client/api/v1/createRoom?access_token=YOUR_ACCESS_TOKEN" { "room_alias": "#tutorial:localhost", @@ -98,20 +105,27 @@ The "room alias" is a human-readable string which can be shared with other users so they can join a room, rather than the room ID which is a randomly generated string. You can have multiple room aliases per room. -TODO(kegan): How to add/remove aliases from an existing room. +.. TODO(kegan) + How to add/remove aliases from an existing room. 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 -XPOST -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8008/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh%3Alocalhost/send/m.room.message?access_token=YOUR_ACCESS_TOKEN" + + { + "event_id": "YUwRidLecu" + } + +The event ID returned is a unique ID which identifies this message. NB: There are no limitations to the types of messages which can be exchanged. -The only requirement is that ``"msgtype"`` is specified. - -NB: Depending on the room config, users who join the room may be able to see -message history from before they joined. +The only requirement is that ``"msgtype"`` is specified. The Matrix +specification outlines the following standard types: ``m.text``, ``m.image``, +``m.audio``, ``m.video``, ``m.location``, ``m.emote``. See the specification for +more information on these types. Users and rooms =============== @@ -121,33 +135,34 @@ these rules may specify if you require an **invitation** from someone already in the room in order to **join the room**. In addition, you may also be able to join a room **via a room alias** if one was set up. -**Try out the fiddle: http://jsfiddle.net/og1xokcr/** +`Try out the fiddle`__ + +.. __: http://jsfiddle.net/7fhotf1b/ 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 -XPOST -d '{"user_id":"@myfriend:localhost"}' "http://localhost:8008/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh%3Alocalhost/invite?access_token=YOUR_ACCESS_TOKEN" This informs ``@myfriend:localhost`` of the room ID ``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room. Joining a room via an invite ---------------------------- -If you receive an invite, you can join the room by changing the membership to -join:: +If you receive an invite, you can join the room:: - curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK" + curl -XPOST -d '{}' "http://localhost:8008/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh%3Alocalhost/join?access_token=YOUR_ACCESS_TOKEN" NB: Only the person invited (``@myfriend:localhost``) can change the membership -state to ``"join"``. +state to ``"join"``. Repeatedly joining a room does nothing. 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 -XPOST -d '{}' "http://localhost:8008/_matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=YOUR_ACCESS_TOKEN" { "room_id": "!CvcvRuDYDzTOzfKKgh:localhost" @@ -166,128 +181,444 @@ An event is some interesting piece of data that a client may be interested in. It can be a message in a room, a room invite, etc. There are many different ways of getting events, depending on what the client already knows. -**Try out the fiddle: http://jsfiddle.net/5uk4dqe2/** +`Try out the fiddle`__ + +.. __: http://jsfiddle.net/vw11mg37/ 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:8008/_matrix/client/api/v1/initialSync?access_token=YOUR_ACCESS_TOKEN" - [ - { - "membership": "join", - "messages": { - "chunk": [ + { + "end": "s39_18_0", + "presence": [ + { + "content": { + "last_active_ago": 1061436, + "user_id": "@example:localhost" + }, + "type": "m.presence" + } + ], + "rooms": [ + { + "membership": "join", + "messages": { + "chunk": [ + { + "content": { + "@example:localhost": 10, + "default": 0 + }, + "event_id": "wAumPSTsWF", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.power_levels", + "user_id": "@example:localhost" + }, + { + "content": { + "join_rule": "public" + }, + "event_id": "jrLVqKHKiI", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.join_rules", + "user_id": "@example:localhost" + }, + { + "content": { + "level": 10 + }, + "event_id": "WpmTgsNWUZ", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.add_state_level", + "user_id": "@example:localhost" + }, + { + "content": { + "level": 0 + }, + "event_id": "qUMBJyKsTQ", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.send_event_level", + "user_id": "@example:localhost" + }, + { + "content": { + "ban_level": 5, + "kick_level": 5 + }, + "event_id": "YAaDmKvoUW", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.ops_levels", + "user_id": "@example:localhost" + }, + { + "content": { + "avatar_url": null, + "displayname": null, + "membership": "join" + }, + "event_id": "RJbPMtCutf", + "membership": "join", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@example:localhost", + "ts": 1409665586730, + "type": "m.room.member", + "user_id": "@example:localhost" + }, + { + "content": { + "body": "hello", + "hsob_ts": 1409665660439, + "msgtype": "m.text" + }, + "event_id": "YUwRidLecu", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "ts": 1409665660439, + "type": "m.room.message", + "user_id": "@example:localhost" + }, + { + "content": { + "membership": "invite" + }, + "event_id": "YjNuBKnPsb", + "membership": "invite", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@myfriend:localhost", + "ts": 1409666426819, + "type": "m.room.member", + "user_id": "@example:localhost" + }, + { + "content": { + "avatar_url": null, + "displayname": null, + "membership": "join", + "prev": "join" + }, + "event_id": "KWwdDjNZnm", + "membership": "join", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@example:localhost", + "ts": 1409666551582, + "type": "m.room.member", + "user_id": "@example:localhost" + }, + { + "content": { + "avatar_url": null, + "displayname": null, + "membership": "join" + }, + "event_id": "JFLVteSvQc", + "membership": "join", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@example:localhost", + "ts": 1409666587265, + "type": "m.room.member", + "user_id": "@example:localhost" + } + ], + "end": "s39_18_0", + "start": "t1-11_18_0" + }, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state": [ { "content": { - "body": "@example:localhost joined the room.", - "hsob_ts": 1408444664249, - "membership": "join", - "membership_source": "@example:localhost", - "membership_target": "@example:localhost", - "msgtype": "m.text" + "creator": "@example:localhost" }, - "event_id": "lZjmmlrEvo", - "msg_id": "m1408444664249", - "room_id": "!CvcvRuDYDzTOzfKKgh:localhost", - "type": "m.room.message", - "user_id": "_homeserver_" + "event_id": "dMUoqVTZca", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.create", + "user_id": "@example:localhost" }, { "content": { - "body": "hello", - "hsob_ts": 1408445405672, - "msgtype": "m.text" + "@example:localhost": 10, + "default": 0 }, - "event_id": "BiBJqamISg", - "msg_id": "msgid1", - "room_id": "!CvcvRuDYDzTOzfKKgh:localhost", - "type": "m.room.message", + "event_id": "wAumPSTsWF", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.power_levels", "user_id": "@example:localhost" }, - [...] { "content": { - "body": "@myfriend:localhost joined the room.", - "hsob_ts": 1408446501661, - "membership": "join", - "membership_source": "@myfriend:localhost", - "membership_target": "@myfriend:localhost", - "msgtype": "m.text" + "join_rule": "public" + }, + "event_id": "jrLVqKHKiI", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.join_rules", + "user_id": "@example:localhost" + }, + { + "content": { + "level": 10 }, - "event_id": "IMmXbOzFAa", - "msg_id": "m1408446501661", - "room_id": "!CvcvRuDYDzTOzfKKgh:localhost", - "type": "m.room.message", - "user_id": "_homeserver_" + "event_id": "WpmTgsNWUZ", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.add_state_level", + "user_id": "@example:localhost" + }, + { + "content": { + "level": 0 + }, + "event_id": "qUMBJyKsTQ", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.send_event_level", + "user_id": "@example:localhost" + }, + { + "content": { + "ban_level": 5, + "kick_level": 5 + }, + "event_id": "YAaDmKvoUW", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.ops_levels", + "user_id": "@example:localhost" + }, + { + "content": { + "membership": "invite" + }, + "event_id": "YjNuBKnPsb", + "membership": "invite", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@myfriend:localhost", + "ts": 1409666426819, + "type": "m.room.member", + "user_id": "@example:localhost" + }, + { + "content": { + "avatar_url": null, + "displayname": null, + "membership": "join" + }, + "event_id": "JFLVteSvQc", + "membership": "join", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@example:localhost", + "ts": 1409666587265, + "type": "m.room.member", + "user_id": "@example:localhost" } - ], - "end": "20", - "start": "0" - }, - "room_id": "!CvcvRuDYDzTOzfKKgh:localhost" - } - ] + ] + } + ] + } -This returns all the room IDs of rooms the user is invited/joined on, as well as -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:: +This returns all the room information the user is invited/joined on, as well as +all of the presences relevant for these rooms. This can be a LOT of data. You +may just want the most recent event for each room. This can be achieved by +applying query parameters to ``limit`` 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:8008/_matrix/client/api/v1/initialSync?limit=1&access_token=YOUR_ACCESS_TOKEN" - [ - { - "membership": "join", - "messages": { - "chunk": [ + { + "end": "s39_18_0", + "presence": [ + { + "content": { + "last_active_ago": 1279484, + "user_id": "@example:localhost" + }, + "type": "m.presence" + } + ], + "rooms": [ + { + "membership": "join", + "messages": { + "chunk": [ + { + "content": { + "avatar_url": null, + "displayname": null, + "membership": "join" + }, + "event_id": "JFLVteSvQc", + "membership": "join", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@example:localhost", + "ts": 1409666587265, + "type": "m.room.member", + "user_id": "@example:localhost" + } + ], + "end": "s39_18_0", + "start": "t10-30_18_0" + }, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state": [ { "content": { - "body": "@myfriend:localhost joined the room.", - "hsob_ts": 1408446501661, - "membership": "join", - "membership_source": "@myfriend:localhost", - "membership_target": "@myfriend:localhost", - "msgtype": "m.text" + "creator": "@example:localhost" + }, + "event_id": "dMUoqVTZca", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.create", + "user_id": "@example:localhost" + }, + { + "content": { + "@example:localhost": 10, + "default": 0 + }, + "event_id": "wAumPSTsWF", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.power_levels", + "user_id": "@example:localhost" + }, + { + "content": { + "join_rule": "public" + }, + "event_id": "jrLVqKHKiI", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.join_rules", + "user_id": "@example:localhost" + }, + { + "content": { + "level": 10 }, - "event_id": "IMmXbOzFAa", - "msg_id": "m1408446501661", - "room_id": "!CvcvRuDYDzTOzfKKgh:localhost", - "type": "m.room.message", - "user_id": "_homeserver_" + "event_id": "WpmTgsNWUZ", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.add_state_level", + "user_id": "@example:localhost" + }, + { + "content": { + "level": 0 + }, + "event_id": "qUMBJyKsTQ", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.send_event_level", + "user_id": "@example:localhost" + }, + { + "content": { + "ban_level": 5, + "kick_level": 5 + }, + "event_id": "YAaDmKvoUW", + "required_power_level": 10, + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "", + "ts": 1409665585188, + "type": "m.room.ops_levels", + "user_id": "@example:localhost" + }, + { + "content": { + "membership": "invite" + }, + "event_id": "YjNuBKnPsb", + "membership": "invite", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@myfriend:localhost", + "ts": 1409666426819, + "type": "m.room.member", + "user_id": "@example:localhost" + }, + { + "content": { + "avatar_url": null, + "displayname": null, + "membership": "join" + }, + "event_id": "JFLVteSvQc", + "membership": "join", + "room_id": "!MkDbyRqnvTYnoxjLYx:localhost", + "state_key": "@example:localhost", + "ts": 1409666587265, + "type": "m.room.member", + "user_id": "@example:localhost" } - ], - "end": "20", - "start": "21" - }, - "room_id": "!CvcvRuDYDzTOzfKKgh:localhost" - } - ] + ] + } + ] + } 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:8008/_matrix/client/api/v1/events?access_token=YOUR_ACCESS_TOKEN" { "chunk": [], - "end": "215", - "start": "215" + "end": "s39_18_0", + "start": "s39_18_0" } This will block waiting for an incoming event, timing out after several seconds. Even if there are no new events (as in the example above), there will be some pagination stream response keys. The client should make subsequent requests -using the value of the ``"end"`` key (in this case ``215``) as the ``from`` -query parameter. This value should be stored so when the client reopens your app -after a period of inactivity, you can resume from where you got up to in the -event stream. If it has been a long period of inactivity, there may be LOTS of -events waiting for the user. In this case, you may wish to get all state instead -and then resume getting live state from a newer end token. +using the value of the ``"end"`` key (in this case ``s39_18_0``) as the ``from`` +query parameter e.g. ``http://localhost:8008/_matrix/client/api/v1/events?access +_token=YOUR_ACCESS_TOKEN&from=s39_18_0``. This value should be stored so when the +client reopens your app after a period of inactivity, you can resume from where +you got up to in the event stream. If it has been a long period of inactivity, +there may be LOTS of events waiting for the user. In this case, you may wish to +get all state instead and then resume getting live state from a newer end token. NB: The timeout can be changed by adding a ``timeout`` query parameter, which is in milliseconds. A timeout of 0 will not block. @@ -300,4 +631,6 @@ creating and joining rooms, sending messages, getting member lists and getting historical messages for a room. This covers most functionality of a messaging application. -**Try out the fiddle: http://jsfiddle.net/L8r3o1wr/** +`Try out the fiddle`__ + +.. __: http://jsfiddle.net/uztL3yme/ diff --git a/docs/specification.rst b/docs/specification.rst index 2b47009187..fdd5f07917 100644 --- a/docs/specification.rst +++ b/docs/specification.rst @@ -1,11 +1,83 @@ Matrix Specification ==================== -TODO(Introduction) : Matthew - - Similar to intro paragraph from README. - - Explaining the overall mission, what this spec describes... - - "What is Matrix?" - - Draw parallels with email? +WARNING +======= + +.. WARNING:: + The Matrix specification is still very much evolving: the API is not yet frozen + and this document is in places incomplete, stale, and may contain security + issues. Needless to say, we have made every effort to highlight the problem + areas that we're aware of. + + We're publishing it at this point because it's complete enough to be more than + useful and provide a canonical reference to how Matrix is evolving. Our end + goal is to mirror WHATWG's `Living Standard <http://wiki.whatwg.org/wiki/FAQ#What_does_.22Living_Standard.22_mean.3F>`_ + approach except right now Matrix is more in the process of being born than actually being + living! + +.. contents:: Table of Contents +.. sectnum:: + +Introduction +============ + +Matrix is a new set of open APIs for open-federated Instant Messaging and VoIP +functionality, designed to create and support a new global real-time +communication ecosystem on the internet. This specification is the ongoing +result of standardising the APIs used by the various components of the Matrix +ecosystem to communicate with one another. + +The principles that Matrix attempts to follow are: + + - Pragmatic Web-friendly APIs (i.e. JSON over REST) + - Keep It Simple & Stupid + + + provide a simple architecture with minimal third-party dependencies. + + - Fully open: + + + Fully open federation - anyone should be able to participate in the global Matrix network + + Fully open standard - publicly documented standard with no IP or patent licensing encumbrances + + Fully open source reference implementation - liberally-licensed example implementations with no + IP or patent licensing encumbrances + + - Empowering the end-user + + + The user should be able to choose the server and clients they use + + The user should be control how private their communication is + + The user should know precisely where their data is stored + + - Fully decentralised - no single points of control over conversations or the network as a whole + - Learning from history to avoid repeating it + + + Trying to take the best aspects of XMPP, SIP, IRC, SMTP, IMAP and NNTP whilst trying to avoid their failings + +The functionality that Matrix provides includes: + + - Creation and management of fully distributed chat rooms with no + single points of control or failure + - Eventually-consistent cryptographically secure synchronisation of room + state across a global open network of federated servers and services + - Sending and receiving extensible messages in a room with (optional) + end-to-end encryption + - Extensible user management (inviting, joining, leaving, kicking, banning) + mediated by a power-level based user privilege system. + - Extensible room state management (room naming, aliasing, topics, bans) + - Extensible user profile management (avatars, displaynames, etc) + - Managing user accounts (registration, login, logout) + - Use of 3rd Party IDs (3PIDs) such as email addresses, phone numbers, + Facebook accounts to authenticate, identify and discover users on Matrix. + - Trusted federation of Identity servers for: + + + Publishing user public keys for PKI + + Mapping of 3PIDs to Matrix IDs + +The end goal of Matrix is to be a ubiquitous messaging layer for synchronising +arbitrary data between sets of people, devices and services - be that for instant +messages, VoIP call setups, or any other objects that need to be reliably and +persistently pushed from A to B in an interoperable and federated manner. + Architecture ============ @@ -28,38 +100,43 @@ other directly. | |<--------( HTTP )-----------| | +------------------+ Federation +------------------+ -A "Client" is an end-user, typically a human using a web application or mobile app. Clients use the -"Client-to-Server" (C-S) API to communicate with their home server. A single Client is usually -responsible for a single user account. A user account is represented by their "User ID". This ID is -namespaced to the home server which allocated the account and looks like:: +A "Client" typically represents a human using a web application or mobile app. Clients use the +"Client-to-Server" (C-S) API to communicate with their home server, which stores their profile data and +their record of the conversations in which they participate. Each client is associated with a user account +(and may optionally support multiple user accounts). A user account is represented by a unique "User ID". This +ID is namespaced to the home server which allocated the account and looks like:: @localpart:domain The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user. They are case-insensitive. +.. TODO + - Need to specify precise grammar for Matrix IDs + A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes. It is typically responsible for multiple clients. "Federation" is the term used to describe the sharing of data between two or more home servers. -Data in Matrix is encapsulated in an "Event". An event is an action within the system. Typically each -action (e.g. sending a message) correlates with exactly one event. Each event has a ``type`` which is -used to differentiate different kinds of data. ``type`` values SHOULD be namespaced according to standard -Java package naming conventions, e.g. ``com.example.myapp.event``. Events are usually sent in the context -of a "Room". +Data in Matrix is encapsulated in an "event". An event is an action within the system. Typically each +action (e.g. sending a message) correlates with exactly one event. Each event has a ``type`` which is used +to differentiate different kinds of data. ``type`` values MUST be uniquely globally namespaced following +Java's `package naming conventions <http://docs.oracle.com/javase/specs/jls/se5.0/html/packages.html#7.7>`, +e.g. ``com.example.myapp.event``. The special top-level namespace ``m.`` is reserved for events defined +in the Matrix specification. Events are usually sent in the context of a "Room". Room structure -------------- A room is a conceptual place where users can send and receive events. Rooms can be created, joined and left. Events are sent to a room, and all -participants in that room will receive the event. Rooms are uniquely -identified via a "Room ID", which look like:: +participants in that room with sufficient access will receive the event. Rooms are uniquely +identified internally via a "Room ID", which look like:: !opaque_id:domain There is exactly one room ID for each room. Whilst the room ID does contain a -domain, it is simply for namespacing room IDs. The room does NOT reside on the +domain, it is simply for globally namespacing room IDs. The room does NOT reside on the domain specified. Room IDs are not meant to be human readable. They ARE case-sensitive. @@ -101,9 +178,12 @@ Each room can also have multiple "Room Aliases", which looks like:: #room_alias:domain -A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained -by visiting the domain specified. Room aliases are designed to be human readable strings -which can be used to publicise rooms. They are case-insensitive. Note that the mapping + .. TODO + - Need to specify precise grammar for Room IDs + +A room alias "points" to a room ID and is the human-readable label by which rooms are +publicised and discovered. The room ID the alias is pointing to can be obtained +by visiting the domain specified. 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. @@ -118,24 +198,61 @@ once and then use that ID on subsequent requests. | domain.com | | Mappings: | | #matrix >> !aaabaa:matrix.org | - | #golf >> !wfeiofh:sport.com | - | #bike >> !4rguxf:matrix.org | + | #golf >> !wfeiofh:sport.com | + | #bike >> !4rguxf:matrix.org | |________________________________| +.. TODO kegan + - show the actual API rather than pseudo-API? + Identity -------- -- Identity in relation to 3PIDs. Discovery of users based on 3PIDs. -- Identity servers; trusted clique of servers which replicate content. -- They govern the mapping of 3PIDs to user IDs and the creation of said mappings. -- Not strictly required in order to communicate. +Users in Matrix are identified via their user ID. However, existing ID namespaces can also +be used in order to identify Matrix users. A Matrix "Identity" describes both the user ID +and any other existing IDs from third party namespaces *linked* to their account. + +Matrix users can *link* third-party IDs (3PIDs) such as email addresses, social +network accounts and phone numbers to their +user ID. Linking 3PIDs creates a mapping from a 3PID to a user ID. This mapping +can then be used by other Matrix users in order to discover other users, according +to a strict set of privacy permissions. + +In order to ensure that the mapping from 3PID to user ID is genuine, a globally federated +cluster of trusted "Identity Servers" (IS) are used to perform authentication of the 3PID. +Identity servers are also used to preserve the mapping indefinitely, by replicating the +mappings across multiple ISes. + +Usage of an IS is not required in order for a client application to be part of +the Matrix ecosystem. However, by not using an IS, discovery of users is greatly +impacted. API Standards ------------- -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:: + +The mandatory baseline for communication in Matrix is exchanging JSON objects over RESTful +HTTP APIs. HTTPS is mandated as the baseline for server-server (federation) communication. +HTTPS is recommended for client-server communication, although HTTP may be supported as a +fallback to support basic HTTP clients. More efficient optional transports for +client-server communication will in future be supported as optional extensions - e.g. a +packed binary encoding over stream-cipher encrypted TCP socket for +low-bandwidth/low-roundtrip mobile usage. + +.. TODO + We need to specify capability negotiation for extensible transports + +For the default HTTP transport, all API calls use a Content-Type of ``application/json``. +In addition, all strings MUST be encoded as UTF-8. + +Clients are authenticated using opaque ``access_token`` strings (see `Registration and +Login`_ for details), passed as a querystring parameter on all requests. + +.. TODO + Need to specify any HMAC or access_token lifetime/ratcheting tricks + +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>", @@ -186,55 +303,57 @@ Some requests have unique error codes: :``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. +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 +unique client-generated transaction ID which identifies the request, and is scoped to a given +Client (identified by that client's ``access_token``). 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; a monotonically increasing integer is +recommended). 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 + POST /some/path/here?access_token=secret { "key": "This is a post." } - PUT /some/path/here/11 + PUT /some/path/here/11?access_token=secret { "key": "This is a put with a txnId of 11." } In contrast, these are invalid requests:: - POST /some/path/here/11 + POST /some/path/here/11?access_token=secret { "key": "This is a post, but it has a txnId." } - PUT /some/path/here + PUT /some/path/here?access_token=secret { "key": "This is a put but it is missing a txnId." } - - -- TODO: All strings everywhere are UTF-8 - - - Receiving live updates on a client ---------------------------------- + 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. +event occurs. This is called the `Event Stream`_. All events which are visible to the +client and match the client's query will appear in the event stream. When the request +returns, an ``end`` token is included in the response. This token can be used in the next +request to continue where the client left off. + +.. TODO + Do we ever return multiple events in a single request? Don't we get lots of request + setup RTT latency if we only do one event per request? Do we ever support streaming + requests? Why not websockets? 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`` +server. This is achieved via the |initialSync|_ API. This API also returns an ``end`` token which can be used with the event stream. Rooms @@ -242,7 +361,10 @@ Rooms Creation -------- -To create a room, a client has to use the ``/createRoom`` API. There are various options +.. TODO kegan + - TODO: Key for invite these users? + +To create a room, a client has to use the |createRoom|_ API. There are various options which can be set when creating a room: ``visibility`` @@ -278,7 +400,7 @@ which can be set when creating a room: 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``. + name of the room. See `Room Events`_ for more information on ``m.room.name``. ``topic`` Type: @@ -289,7 +411,7 @@ which can be set when creating a room: 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``. + topic for the room. See `Room Events`_ for more information on ``m.room.topic``. Example:: @@ -300,35 +422,59 @@ Example:: "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? +The home server will create a ``m.room.create`` event when the room is +created, which serves as the root of the PDU graph for this room. This +event also has a ``creator`` key which contains the user ID of the room +creator. It will also generate several other events in order to manage +permissions in this room. This includes: + + - ``m.room.power_levels`` : Sets the authority of the room creator. + - ``m.room.join_rules`` : Whether the room is "invite-only" or not. + - ``m.room.add_state_level`` + - ``m.room.send_event_level`` : The power level required in order to + send a message in this room. + - ``m.room.ops_level`` : The power level required in order to kick or + ban a user from the room. + +See `Room Events`_ for more information on these events. Modifying aliases ----------------- -- path to edit aliases -- format when retrieving list of aliases. NOT complete list. -- format for adding aliases. +.. NOTE:: + This section is a work in progress. + +.. TODO kegan + - path to edit aliases + - PUT /directory/room/<room alias> { room_id : foo } + - GET /directory/room/<room alias> { room_id : foo, servers: [a.com, b.com] } + - format when retrieving list of aliases. NOT complete list. + - format for adding/removing 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. +.. NOTE:: + This section is a work in progress. + +.. TODO kegan + - 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? +.. TODO kegan + - 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:: +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. +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:: @@ -345,19 +491,21 @@ by sending the following request to "membership": "join" } -See the "Room events" section for more information on ``m.room.member``. +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. +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? +.. TODO kegan + - 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? + - TODO: In what circumstances will direct member editing NOT be equivalent to ``/invite``? 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 @@ -372,7 +520,7 @@ 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 +following request to |/rooms/<room_id>/invite|_, which will manage the entire invitation process:: { @@ -387,16 +535,19 @@ directly by sending the following request to "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``? +See the `Room events`_ section for more information on ``m.room.member``. Leaving rooms ------------- +.. TODO kegan + - TODO: Grace period before deletion? + - TODO: Under what conditions should a room NOT be purged? + + 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:: +To leave a room, a request should be made to |/rooms/<room_id>/leave|_ with:: {} @@ -408,9 +559,9 @@ directly by sending the following request to "membership": "leave" } -See the "Room events" section for more information on ``m.room.member``. +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`` +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 @@ -418,18 +569,15 @@ 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:: +|/rooms/<room_id>/ban|_ with:: { "user_id": "<user id to ban" @@ -469,7 +617,7 @@ risk of clashes. State events ------------ -State events can be sent by ``PUT`` ing to ``/rooms/<room id>/state/<event type>/<state key>``. +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 @@ -506,11 +654,11 @@ 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. +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>``. +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:: @@ -521,23 +669,27 @@ For example:: PUT /rooms/!roomid:domain/send/m.custom.example.message/11 { "text": "Goodbye world!" } -See "Room Events" for the ``m.`` event specification. +See `Room Events`_ for the ``m.`` event specification. Syncing rooms ------------- +.. NOTE:: + This section is a work in progress. + 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 + - |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 + - |/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? +.. TODO kegan + - TODO: JSON response format for both types + - TODO: when would you use global? when would you use scoped? Getting events for a room ------------------------- @@ -551,7 +703,7 @@ There are several APIs provided to ``GET`` events for a room: Example: ``/rooms/!room:domain.com/state/m.room.name`` returns ``{ "name": "Room name" }`` -``/rooms/<room id>/state`` +|/rooms/<room_id>/state|_ Description: Get all state events for a room. Response format: @@ -560,7 +712,7 @@ There are several APIs provided to ``GET`` events for a room: TODO -``/rooms/<room id>/members`` +|/rooms/<room_id>/members|_ Description: Get all ``m.room.member`` state events. Response format: @@ -568,7 +720,7 @@ There are several APIs provided to ``GET`` events for a room: Example: TODO -``/rooms/<room id>/messages`` +|/rooms/<room_id>/messages|_ Description: Get all ``m.room.message`` events. Response format: @@ -576,7 +728,7 @@ There are several APIs provided to ``GET`` events for a room: Example: TODO -``/rooms/<room id>/initialSync`` +|/rooms/<room_id>/initialSync|_ Description: Get all relevant events for a room. This includes state events, paginated non-state events and presence events. @@ -588,7 +740,11 @@ There are several APIs provided to ``GET`` events for a room: Room Events =========== -- voip events? +.. NOTE:: + This section is a work in progress. + +.. TODO dave? + - voip events? This specification outlines several standard event types, all of which are prefixed with ``m.`` @@ -607,7 +763,7 @@ prefixed with ``m.`` 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. + creating a room using |createRoom|_ with the ``name`` key. ``m.room.topic`` Summary: @@ -621,7 +777,8 @@ prefixed with ``m.`` 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. + not be suitable for the room name. The room topic can also be set when creating a + room using |createRoom|_ with the ``topic`` key. ``m.room.member`` Summary: @@ -637,7 +794,7 @@ prefixed with ``m.`` 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 + to force this state change directly will fail. See the `Rooms`_ section for how to use the membership APIs. ``m.room.config`` @@ -650,7 +807,7 @@ prefixed with ``m.`` Example: TODO Description: - TODO + TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what ``m.room.invite_join`` Summary: @@ -662,7 +819,67 @@ prefixed with ``m.`` Example: TODO Description: + TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what + +``m.room.join_rules`` + Summary: + TODO. + Type: + State event + JSON format: + TODO + Example: + TODO + Description: + TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what + +``m.room.power_levels`` + Summary: + TODO. + Type: + State event + JSON format: + TODO + Example: + TODO + Description: + TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what + +``m.room.add_state_level`` + Summary: + TODO. + Type: + State event + JSON format: + TODO + Example: + TODO + Description: + TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what + +``m.room.send_event_level`` + Summary: + TODO. + Type: + State event + JSON format: + TODO + Example: + TODO + Description: + TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what + +``m.room.ops_levels`` + Summary: + TODO. + Type: + State event + JSON format: TODO + Example: + TODO + Description: + TODO : What it represents, What are the valid keys / values and what they represent, When is this event emitted and by what ``m.room.message`` Summary: @@ -678,7 +895,7 @@ prefixed with ``m.`` 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". + the types of messages which can be sent, see `m.room.message msgtypes`_. ``m.room.message.feedback`` Summary: @@ -799,6 +1016,8 @@ The following keys can be attached to any ``m.room.message``: Presence ======== +.. NOTE:: + This section is a work in progress. Each user has the concept of presence information. This encodes the "availability" of that user, suitable for display on other user's clients. This @@ -837,8 +1056,12 @@ user was last seen online. Transmission ------------ -- Transmitted as an EDU. -- Presence lists determine who to send to. +.. NOTE:: + This section is a work in progress. + +.. TODO: + - Transmitted as an EDU. + - Presence lists determine who to send to. Presence List ------------- @@ -863,28 +1086,43 @@ 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. +.. NOTE:: + This section is a work in progress. -TODO : Leo +.. TODO Leo + - 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. 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 +.. NOTE:: + This section is a work in progress. + +.. TODO Dave + - 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? Profiles ======== +.. NOTE:: + This section is a work in progress. + +.. TODO + - Metadata extensibility + - 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. Internally within Matrix users are referred to by their user ID, which is not a human-friendly string. Profiles grant users the ability to see human-readable @@ -896,31 +1134,29 @@ metadata fields that the user may wish to publish (email address, phone numbers, website URLs, etc...). This specification puts no requirements on the display name other than it being a valid unicode string. -- Metadata extensibility -- 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 ====================== +.. WARNING:: + The registration API is likely to change. + +.. TODO + - TODO Kegan : Make registration like login (just omit the "user" key on the + initial request?) Clients must register with a home server in order to use Matrix. After registering, the client will be given an access token which must be used in ALL requests to that home server as a query parameter 'access_token'. -- TODO Kegan : Make registration like login (just omit the "user" key on the - initial request?) - If the client has already registered, they need to be able to login to their account. The home server may provide many different ways of logging in, such as user/password auth, login via a social network (OAuth2), login by confirming a token sent to their email address, etc. This specification does not define how home servers should authorise their users who want to login to their existing accounts, but instead defines the standard interface which implementations -should follow so that ANY client can login to ANY home server. +should follow so that ANY client can login to ANY home server. Clients login +using the |login|_ API. The login process breaks down into the following: 1. Determine the requirements for logging in. @@ -985,7 +1221,7 @@ This specification defines the following login types: Password-based -------------- :Type: - m.login.password + ``m.login.password`` :Description: Login is supported via a username and password. @@ -1003,7 +1239,7 @@ process, or a standard error response. OAuth2-based ------------ :Type: - m.login.oauth2 + ``m.login.oauth2`` :Description: Login is supported via OAuth2 URLs. This login consists of multiple requests. @@ -1056,7 +1292,7 @@ visits the REDIRECT_URI with the auth code= query parameter which returns:: Email-based (code) ------------------ :Type: - m.login.email.code + ``m.login.email.code`` :Description: Login is supported by typing in a code which is sent in an email. This login consists of multiple requests. @@ -1091,7 +1327,7 @@ the login process, or a standard error response. Email-based (url) ----------------- :Type: - m.login.email.url + ``m.login.email.url`` :Description: Login is supported by clicking on a URL in an email. This login consists of multiple requests. @@ -1190,9 +1426,11 @@ This MUST return an HTML page which can perform the entire login process. Identity ======== +.. NOTE:: + This section is a work in progress. -TODO : Dave -- 3PIDs and identity server, functions +.. TODO Dave + - 3PIDs and identity server, functions Federation ========== @@ -1233,6 +1471,9 @@ transferred from the origin to the destination home server using an HTTP PUT req Transactions ------------ +.. WARNING:: + This section may be misleading or inaccurate. + The transfer of EDUs and PDUs between home servers is performed by an exchange of Transaction messages, which are encoded as JSON objects, passed over an HTTP PUT request. A Transaction is meaningful only to the pair of home servers that @@ -1275,6 +1516,8 @@ mechanism to encourage peers to continue to replicate content.) PDUs and EDUs ------------- +.. WARNING:: + This section may be misleading or inaccurate. All PDUs have: - An ID @@ -1345,43 +1588,129 @@ destination home server names, and the actual nested content. Backfilling ----------- -- What it is, when is it used, how is it done +.. NOTE:: + This section is a work in progress. + +.. TODO + - What it is, when is it used, how is it done SRV Records ----------- -- Why it is needed +.. NOTE:: + This section is a work in progress. + +.. TODO + - Why it is needed Security ======== -- rate limiting -- crypto (s-s auth) -- E2E -- Lawful intercept + Key Escrow +.. NOTE:: + This section is a work in progress. -TODO Mark +Rate limiting +------------- +Home servers SHOULD implement rate limiting to reduce the risk of being overloaded. If a +request is refused due to rate limiting, it should return a standard error response of +the form:: + + { + "errcode": "M_LIMIT_EXCEEDED", + "error": "string", + "retry_after_ms": integer (optional) + } + +The ``retry_after_ms`` key SHOULD be included to tell the client how long they have to wait +in milliseconds before they can try again. + +.. TODO + - crypto (s-s auth) + - E2E + - Lawful intercept + Key Escrow + TODO Mark Policy Servers ============== -TODO +.. NOTE:: + This section is a work in progress. Content repository ================== -- path to upload -- format for thumbnail paths, mention what it is protecting against. -- content size limit and associated M_ERROR. +.. NOTE:: + This section is a work in progress. + +.. TODO + - path to upload + - format for thumbnail paths, mention what it is protecting against. + - content size limit and associated M_ERROR. Address book repository ======================= -- 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? +.. NOTE:: + This section is a work in progress. + +.. TODO + - 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 ======== -- domain specific words/acronyms with definitions +.. NOTE:: + This section is a work in progress. + +.. TODO + - domain specific words/acronyms with definitions User ID: An opaque ID which identifies an end-user, which consists of some opaque localpart combined with the domain name of their home server. + + +.. Links through the external API docs are below +.. ============================================= + +.. |createRoom| replace:: ``/createRoom`` +.. _createRoom: /-rooms/create_room + +.. |initialSync| replace:: ``/initialSync`` +.. _initialSync: /-events/initial_sync + +.. |/rooms/<room_id>/initialSync| replace:: ``/rooms/<room_id>/initialSync`` +.. _/rooms/<room_id>/initialSync: /-rooms/get_room_sync_data + +.. |login| replace:: ``/login`` +.. _login: /-login + +.. |/rooms/<room_id>/messages| replace:: ``/rooms/<room_id>/messages`` +.. _/rooms/<room_id>/messages: /-rooms/get_messages + +.. |/rooms/<room_id>/members| replace:: ``/rooms/<room_id>/members`` +.. _/rooms/<room_id>/members: /-rooms/get_members + +.. |/rooms/<room_id>/state| replace:: ``/rooms/<room_id>/state`` +.. _/rooms/<room_id>/state: /-rooms/get_state_events + +.. |/rooms/<room_id>/send/<event_type>| replace:: ``/rooms/<room_id>/send/<event_type>`` +.. _/rooms/<room_id>/send/<event_type>: /-rooms/send_non_state_event + +.. |/rooms/<room_id>/state/<event_type>/<state_key>| replace:: ``/rooms/<room_id>/state/<event_type>/<state_key>`` +.. _/rooms/<room_id>/state/<event_type>/<state_key>: /-rooms/send_state_event + +.. |/rooms/<room_id>/invite| replace:: ``/rooms/<room_id>/invite`` +.. _/rooms/<room_id>/invite: /-rooms/invite + +.. |/rooms/<room_id>/join| replace:: ``/rooms/<room_id>/join`` +.. _/rooms/<room_id>/join: /-rooms/join_room + +.. |/rooms/<room_id>/leave| replace:: ``/rooms/<room_id>/leave`` +.. _/rooms/<room_id>/leave: /-rooms/leave + +.. |/rooms/<room_id>/ban| replace:: ``/rooms/<room_id>/ban`` +.. _/rooms/<room_id>/ban: /-rooms/ban + +.. |/join/<room_alias_or_id>| replace:: ``/join/<room_alias_or_id>`` +.. _/join/<room_alias_or_id>: /-rooms/join + +.. _`Event Stream`: /-events/get_event_stream diff --git a/jsfiddles/create_room_send_msg/demo.html b/jsfiddles/create_room_send_msg/demo.html index 31c26c7631..088ff7ac0f 100644 --- a/jsfiddles/create_room_send_msg/demo.html +++ b/jsfiddles/create_room_send_msg/demo.html @@ -1,5 +1,5 @@ <div> - <p>This room creation / message sending demo requires a home server to be running on http://localhost:8080</p> + <p>This room creation / message sending demo requires a home server to be running on http://localhost:8008</p> </div> <form class="loginForm"> <input type="text" id="userLogin" placeholder="Username"></input> diff --git a/jsfiddles/create_room_send_msg/demo.js b/jsfiddles/create_room_send_msg/demo.js index 61044da743..3dc7263830 100644 --- a/jsfiddles/create_room_send_msg/demo.js +++ b/jsfiddles/create_room_send_msg/demo.js @@ -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:8008/_matrix/client/api/v1/login", type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), @@ -25,7 +25,7 @@ $('.login').live('click', function() { }); var getCurrentRoomList = function() { - var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; + var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; $.getJSON(url, function(data) { var rooms = data.rooms; for (var i=0; i<rooms.length; ++i) { @@ -44,7 +44,7 @@ $('.createRoom').live('click', function() { data.room_alias_name = roomAlias; } $.ajax({ - url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, + url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify(data), @@ -79,7 +79,7 @@ $('.sendMessage').live('click', function() { return; } - var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; + var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; url = url.replace("$token", accountInfo.access_token); url = url.replace("$roomid", encodeURIComponent(roomId)); diff --git a/jsfiddles/event_stream/demo.html b/jsfiddles/event_stream/demo.html index ee4fc3ea68..7657780d28 100644 --- a/jsfiddles/event_stream/demo.html +++ b/jsfiddles/event_stream/demo.html @@ -1,5 +1,5 @@ <div> - <p>This event stream demo requires a home server to be running on http://localhost:8080</p> + <p>This event stream demo requires a home server to be running on http://localhost:8008</p> </div> <form class="loginForm"> <input type="text" id="userLogin" placeholder="Username"></input> diff --git a/jsfiddles/event_stream/demo.js b/jsfiddles/event_stream/demo.js index 997d1a2240..5c81e08caa 100644 --- a/jsfiddles/event_stream/demo.js +++ b/jsfiddles/event_stream/demo.js @@ -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:8008/_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:8008/_matrix/client/api/v1/login", type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), @@ -65,7 +65,7 @@ $('.login').live('click', function() { var getCurrentRoomList = function() { $("#roomId").val(""); - var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; + var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; $.getJSON(url, function(data) { var rooms = data.rooms; for (var i=0; i<rooms.length; ++i) { @@ -98,7 +98,7 @@ var sendMessage = function(roomId) { return; } - var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; + var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; url = url.replace("$token", accountInfo.access_token); url = url.replace("$roomid", encodeURIComponent(roomId)); diff --git a/jsfiddles/example_app/demo.html b/jsfiddles/example_app/demo.html index 0af946f6bc..7a9dffddd0 100644 --- a/jsfiddles/example_app/demo.html +++ b/jsfiddles/example_app/demo.html @@ -1,5 +1,5 @@ <div class="signUp"> - <p>Matrix example application: Requires a local home server running at http://localhost:8080</p> + <p>Matrix example application: Requires a local home server running at http://localhost:8008</p> <form class="registrationForm"> <p>No account? Register:</p> <input type="text" id="userReg" placeholder="Username"></input> diff --git a/jsfiddles/example_app/demo.js b/jsfiddles/example_app/demo.js index 958232047f..ad79fcca26 100644 --- a/jsfiddles/example_app/demo.js +++ b/jsfiddles/example_app/demo.js @@ -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:8008/_matrix/client/api/v1/events?access_token=$token&from=$from"; url = url.replace("$token", accountInfo.access_token); url = url.replace("$from", eventStreamInfo.from); @@ -89,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:8008/_matrix/client/api/v1/login", type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), @@ -107,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:8008/_matrix/client/api/v1/register", type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify({ user_id: user, password: password }), @@ -134,7 +134,7 @@ $('.createRoom').live('click', function() { data.room_alias_name = roomAlias; } $.ajax({ - url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, + url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify(data), @@ -155,7 +155,7 @@ $('.createRoom').live('click', function() { // ************** Getting current state ************** var getCurrentRoomList = function() { - var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; + var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; $.getJSON(url, function(data) { var rooms = data.rooms; for (var i=0; i<rooms.length; ++i) { @@ -181,7 +181,7 @@ var loadRoomContent = function(roomId) { var getMessages = function(roomId) { $("#messages").empty(); - var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" + + var url = "http://localhost:8008/_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) { @@ -193,7 +193,7 @@ var getMessages = function(roomId) { var getMemberList = function(roomId) { $("#members").empty(); memberInfo = []; - var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" + + var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" + encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token; $.getJSON(url, function(data) { for (var i=0; i<data.chunk.length; ++i) { @@ -216,7 +216,7 @@ $('.sendMessage').live('click', function() { var sendMessage = function(roomId, body) { var msgId = $.now(); - var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; + var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token"; url = url.replace("$token", accountInfo.access_token); url = url.replace("$roomid", encodeURIComponent(roomId)); @@ -262,7 +262,7 @@ 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/join?access_token=$token"; + var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token"; url = url.replace("$token", accountInfo.access_token); url = url.replace("$roomid", encodeURIComponent(roomId)); $.ajax({ @@ -290,6 +290,9 @@ var addMessage = function(data) { var msg = data.content.body; if (data.type === "m.room.member") { + if (data.content.membership === undefined) { + return; + } if (data.content.membership === "invite") { msg = "<em>invited " + data.state_key + " to the room</em>"; } @@ -299,10 +302,13 @@ var addMessage = function(data) { else if (data.content.membership === "leave") { msg = "<em>left the room</em>"; } - else { - msg = "<em>" + data.content.membership + "</em>"; + else if (data.content.membership === "ban") { + msg = "<em>was banned from the room</em>"; } } + if (msg === undefined) { + return; + } var row = "<tr>" + "<td>"+data.user_id+"</td>" + diff --git a/jsfiddles/register_login/demo.html b/jsfiddles/register_login/demo.html index 9cdb161306..fcac453ac2 100644 --- a/jsfiddles/register_login/demo.html +++ b/jsfiddles/register_login/demo.html @@ -1,5 +1,5 @@ <div> - <p>This registration/login demo requires a home server to be running on http://localhost:8080</p> + <p>This registration/login demo requires a home server to be running on http://localhost:8008</p> </div> <form class="registrationForm"> <input type="text" id="user" placeholder="Username"></input> diff --git a/jsfiddles/register_login/demo.js b/jsfiddles/register_login/demo.js index 1e68cb91bd..9595039173 100644 --- a/jsfiddles/register_login/demo.js +++ b/jsfiddles/register_login/demo.js @@ -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:8008/_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:8008/_matrix/client/api/v1/login", type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), @@ -44,7 +44,7 @@ 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) { + $.getJSON("http://localhost:8008/_matrix/client/api/v1/login", function(data) { if (data.flows[0].type !== "m.login.password") { alert("I don't know how to login with this type: " + data.type); return; @@ -60,7 +60,7 @@ $('.logout').live('click', function() { }); $('.testToken').live('click', function() { - var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; + var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; $.getJSON(url, function(data) { $("#imSyncText").text(JSON.stringify(data, undefined, 2)); }).fail(function(err) { diff --git a/jsfiddles/room_memberships/demo.html b/jsfiddles/room_memberships/demo.html index 4c1bf6b4bb..e6f39df5aa 100644 --- a/jsfiddles/room_memberships/demo.html +++ b/jsfiddles/room_memberships/demo.html @@ -1,5 +1,5 @@ <div> - <p>This room membership demo requires a home server to be running on http://localhost:8080</p> + <p>This room membership demo requires a home server to be running on http://localhost:8008</p> </div> <form class="loginForm"> <input type="text" id="userLogin" placeholder="Username"></input> diff --git a/jsfiddles/room_memberships/demo.js b/jsfiddles/room_memberships/demo.js index 7e499049ab..64ba767138 100644 --- a/jsfiddles/room_memberships/demo.js +++ b/jsfiddles/room_memberships/demo.js @@ -18,7 +18,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:8008/_matrix/client/api/v1/login", type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify({ user: user, password: password, type: "m.login.password" }), @@ -39,7 +39,7 @@ 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/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; + var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; $.getJSON(url, function(data) { var rooms = data.rooms; for (var i=0; i<rooms.length; ++i) { @@ -53,7 +53,7 @@ var getCurrentRoomList = function() { $('.createRoom').live('click', function() { var data = {}; $.ajax({ - url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, + url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token, type: "POST", contentType: "application/json; charset=utf-8", data: JSON.stringify(data), @@ -87,7 +87,7 @@ $('.changeMembership').live('click', function() { return; } - var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token"; + var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token"; url = url.replace("$token", accountInfo.access_token); url = url.replace("$roomid", encodeURIComponent(roomId)); url = url.replace("$membership", membership); @@ -117,7 +117,7 @@ $('.changeMembership').live('click', function() { $('.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:8008/_matrix/client/api/v1/join/$roomalias?access_token=$token"; url = url.replace("$token", accountInfo.access_token); url = url.replace("$roomalias", encodeURIComponent(roomAlias)); $.ajax({ diff --git a/scripts/basic.css b/scripts/basic.css new file mode 100644 index 0000000000..6411570ee6 --- /dev/null +++ b/scripts/basic.css @@ -0,0 +1,510 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +img { + border: 0; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.document p.caption { + text-align: inherit; +} + +div.document td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +.align-left { + text-align: left; +} + +.align-center { + clear: both; + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.document p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlighted { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.refcount { + color: #060; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.document div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} + diff --git a/scripts/gendoc.sh b/scripts/gendoc.sh new file mode 100755 index 0000000000..30ba1db629 --- /dev/null +++ b/scripts/gendoc.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +MATRIXDOTORG=$HOME/workspace/matrix.org + +rst2html-2.7.py --stylesheet=basic.css,nature.css ../docs/specification.rst > $MATRIXDOTORG/docs/spec/index.html +rst2html-2.7.py --stylesheet=basic.css,nature.css ../docs/client-server/howto.rst > $MATRIXDOTORG/docs/howtos/client-server.html + +perl -pi -e 's#<head>#<head><link rel="stylesheet" href="/site.css">#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html + +perl -pi -e 's#<body>#<body><div id="header"><div id="headerContent"> </div></div><div id="page"><div id="wrapper"><div style="text-align: center; padding: 40px;"><img src="/matrix.png" width="305" height="130" alt="[matrix]"/></div>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html + +perl -pi -e 's#</body>#</div></div><div id="footer"><div id="footerContent">© 2014 Matrix.org</div></div></body>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html \ No newline at end of file diff --git a/scripts/nature.css b/scripts/nature.css new file mode 100644 index 0000000000..b8147f10ee --- /dev/null +++ b/scripts/nature.css @@ -0,0 +1,270 @@ +/* + * nature.css_t + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- nature theme. + * + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Arial, sans-serif; + font-size: 100%; + /*background-color: #111;*/ + color: #555; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +hr { + border: 1px solid #B1B4B6; +} + +/* +div.document { + background-color: #eee; +} +*/ + +div.document { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; + font-size: 0.9em; +} + +div.footer { + color: #555; + width: 100%; + padding: 13px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #444; + text-decoration: underline; +} + +div.related { + background-color: #6BA81E; + line-height: 32px; + color: #fff; + text-shadow: 0px 1px 0 #444; + font-size: 0.9em; +} + +div.related a { + color: #E2F3CC; +} + +div.sphinxsidebar { + font-size: 0.75em; + line-height: 1.5em; +} + +div.sphinxsidebarwrapper{ + padding: 20px 0; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Arial, sans-serif; + color: #222; + font-size: 1.2em; + font-weight: normal; + margin: 0; + padding: 5px 10px; + background-color: #ddd; + text-shadow: 1px 1px 0 white +} + +div.sphinxsidebar h4{ + font-size: 1.1em; +} + +div.sphinxsidebar h3 a { + color: #444; +} + + +div.sphinxsidebar p { + color: #888; + padding: 5px 20px; +} + +div.sphinxsidebar p.topless { +} + +div.sphinxsidebar ul { + margin: 10px 20px; + padding: 0; + color: #000; +} + +div.sphinxsidebar a { + color: #444; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar input[type=text]{ + margin-left: 20px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #005B81; + text-decoration: none; +} + +a:hover { + color: #E32E00; + text-decoration: underline; +} + +div.document h1, +div.document h2, +div.document h3, +div.document h4, +div.document h5, +div.document h6 { + font-family: Arial, sans-serif; + background-color: #BED4EB; + font-weight: normal; + color: #212224; + margin: 30px 0px 10px 0px; + padding: 5px 0 5px 10px; + text-shadow: 0px 1px 0 white +} + +div.document h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } +div.document h2 { font-size: 150%; background-color: #C8D5E3; } +div.document h3 { font-size: 120%; background-color: #D8DEE3; } +div.document h4 { font-size: 110%; background-color: #D8DEE3; } +div.document h5 { font-size: 100%; background-color: #D8DEE3; } +div.document h6 { font-size: 100%; background-color: #D8DEE3; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.document p, div.document dd, div.document li { + line-height: 1.5em; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.highlight{ + background-color: white; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 10px; + background-color: White; + color: #222; + line-height: 1.2em; + border: 1px solid #C6C9CB; + font-size: 1.1em; + margin: 1.5em 0 1.5em 0; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ + font-size: 1.1em; + font-family: monospace; +} + +.viewcode-back { + font-family: Arial, sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} + +p { + margin: 0; +} + +ul li dd { + margin-top: 0; +} + +ul li dl { + margin-bottom: 0; +} + +li dl dd { + margin-bottom: 0; +} + +dd ul { + padding-left: 0; +} + +li dd ul { + margin-bottom: 0; +} + diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 21ededc5ae..ad79bc7ff9 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -28,6 +28,7 @@ class Codes(object): UNKNOWN = "M_UNKNOWN" NOT_FOUND = "M_NOT_FOUND" UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" + LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" class CodeMessageException(Exception): @@ -38,11 +39,15 @@ class CodeMessageException(Exception): super(CodeMessageException, self).__init__("%d: %s" % (code, msg)) self.code = code self.msg = msg + self.response_code_message = None + + def error_dict(self): + return cs_error(self.msg) class SynapseError(CodeMessageException): """A base error which can be caught for all synapse events.""" - def __init__(self, code, msg, errcode=""): + def __init__(self, code, msg, errcode=Codes.UNKNOWN): """Constructs a synapse error. Args: @@ -53,6 +58,11 @@ class SynapseError(CodeMessageException): super(SynapseError, self).__init__(code, msg) self.errcode = errcode + def error_dict(self): + return cs_error( + self.msg, + self.errcode, + ) class RoomError(SynapseError): """An error raised when a room event fails.""" @@ -91,13 +101,26 @@ class StoreError(SynapseError): pass -def cs_exception(exception): - if isinstance(exception, SynapseError): +class LimitExceededError(SynapseError): + """A client has sent too many requests and is being throttled. + """ + def __init__(self, code=429, msg="Too Many Requests", retry_after_ms=None, + errcode=Codes.LIMIT_EXCEEDED): + super(LimitExceededError, self).__init__(code, msg, errcode) + self.retry_after_ms = retry_after_ms + self.response_code_message = "Too Many Requests" + + def error_dict(self): return cs_error( - exception.msg, - Codes.UNKNOWN if not exception.errcode else exception.errcode) - elif isinstance(exception, CodeMessageException): - return cs_error(exception.msg) + self.msg, + self.errcode, + retry_after_ms=self.retry_after_ms, + ) + + +def cs_exception(exception): + if isinstance(exception, CodeMessageException): + return exception.error_dict() else: logging.error("Unknown exception type: %s", type(exception)) diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py new file mode 100644 index 0000000000..b9b6a9de1a --- /dev/null +++ b/synapse/api/ratelimiting.py @@ -0,0 +1,79 @@ +# 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 collections + + +class Ratelimiter(object): + """ + Ratelimit message sending by user. + """ + + def __init__(self): + self.message_counts = collections.OrderedDict() + + def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count): + """Can the user send a message? + Args: + user_id: The user sending a message. + time_now_s: The time now. + msg_rate_hz: The long term number of messages a user can send in a + second. + burst_count: How many messages the user can send before being + limited. + Returns: + A pair of a bool indicating if they can send a message now and a + time in seconds of when they can next send a message. + """ + self.prune_message_counts(time_now_s) + message_count, time_start, _ignored = self.message_counts.pop( + user_id, (0., time_now_s, None), + ) + time_delta = time_now_s - time_start + sent_count = message_count - time_delta * msg_rate_hz + if sent_count < 0: + allowed = True + time_start = time_now_s + message_count = 1. + elif sent_count > burst_count - 1.: + allowed = False + else: + allowed = True + message_count += 1 + + self.message_counts[user_id] = ( + message_count, time_start, msg_rate_hz + ) + + if msg_rate_hz > 0: + time_allowed = ( + time_start + (message_count - burst_count + 1) / msg_rate_hz + ) + if time_allowed < time_now_s: + time_allowed = time_now_s + else: + time_allowed = -1 + + return allowed, time_allowed + + def prune_message_counts(self, time_now_s): + for user_id in self.message_counts.keys(): + message_count, time_start, msg_rate_hz = ( + self.message_counts[user_id] + ) + time_delta = time_now_s - time_start + if message_count - time_delta * msg_rate_hz > 0: + break + else: + del self.message_counts[user_id] diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 606c9c650d..8a7cd07fec 100755 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -247,6 +247,7 @@ def setup(): upload_dir=os.path.abspath("uploads"), db_name=config.database_path, tls_context_factory=tls_context_factory, + config=config, ) hs.register_servlets() diff --git a/synapse/config/_base.py b/synapse/config/_base.py index 1913179c3a..4b445806da 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - -import ConfigParser as configparser import argparse import sys import os diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 18072e3196..a9aa4c735c 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -17,8 +17,10 @@ from .tls import TlsConfig from .server import ServerConfig from .logger import LoggingConfig from .database import DatabaseConfig +from .ratelimiting import RatelimitConfig -class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig): +class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, + RatelimitConfig): pass if __name__=='__main__': diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py new file mode 100644 index 0000000000..ee572e1fbf --- /dev/null +++ b/synapse/config/ratelimiting.py @@ -0,0 +1,35 @@ +# 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 + +class RatelimitConfig(Config): + + def __init__(self, args): + super(RatelimitConfig, self).__init__(args) + self.rc_messages_per_second = args.rc_messages_per_second + self.rc_message_burst_count = args.rc_message_burst_count + + @classmethod + def add_arguments(cls, parser): + super(RatelimitConfig, cls).add_arguments(parser) + rc_group = parser.add_argument_group("ratelimiting") + rc_group.add_argument( + "--rc-messages-per-second", type=float, default=0.2, + help="number of messages a client can send per second" + ) + rc_group.add_argument( + "--rc-message-burst-count", type=float, default=10, + help="number of message a client can send before being throttled" + ) diff --git a/synapse/crypto/context_factory.py b/synapse/crypto/context_factory.py index 45958abbf5..344f2dd218 100644 --- a/synapse/crypto/context_factory.py +++ b/synapse/crypto/context_factory.py @@ -1,4 +1,18 @@ -from twisted.internet import reactor, ssl +# Copyright 2014 matrix.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import ssl from OpenSSL import SSL from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index b37c8be964..57429ad2ef 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -14,6 +14,7 @@ # limitations under the License. from twisted.internet import defer +from synapse.api.errors import LimitExceededError class BaseHandler(object): @@ -25,8 +26,22 @@ class BaseHandler(object): self.room_lock = hs.get_room_lock_manager() self.state_handler = hs.get_state_handler() self.distributor = hs.get_distributor() + self.ratelimiter = hs.get_ratelimiter() + self.clock = hs.get_clock() self.hs = hs + def ratelimit(self, user_id): + time_now = self.clock.time() + allowed, time_allowed = self.ratelimiter.send_message( + user_id, time_now, + msg_rate_hz=self.hs.config.rc_messages_per_second, + burst_count=self.hs.config.rc_message_burst_count, + ) + if not allowed: + raise LimitExceededError( + retry_after_ms=int(1000*(time_allowed - time_now)), + ) + class BaseRoomHandler(BaseHandler): diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 4aeb2089f5..c9e3c4e451 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -76,6 +76,8 @@ class MessageHandler(BaseRoomHandler): Raises: SynapseError if something went wrong. """ + + self.ratelimit(event.user_id) # 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,) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 53aa77405c..58afced8f5 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -49,6 +49,7 @@ class RoomCreationHandler(BaseRoomHandler): SynapseError if the room ID was taken, couldn't be stored, or something went horribly wrong. """ + self.ratelimit(user_id) if "room_alias_name" in config: room_alias = RoomAlias.create_local( @@ -110,8 +111,6 @@ class RoomCreationHandler(BaseRoomHandler): servers=[self.hs.hostname], ) - federation_handler = self.hs.get_handlers().federation_handler - @defer.inlineCallbacks def handle_event(event): snapshot = yield self.store.snapshot_room( diff --git a/synapse/http/server.py b/synapse/http/server.py index 0b87718bfa..74c220e869 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -140,7 +140,8 @@ class JsonResource(HttpServer, resource.Resource): self._send_response( request, e.code, - cs_exception(e) + cs_exception(e), + response_code_message=e.response_code_message ) except Exception as e: logger.exception(e) @@ -150,7 +151,8 @@ class JsonResource(HttpServer, resource.Resource): {"error": "Internal server error"} ) - def _send_response(self, request, code, response_json_object): + def _send_response(self, request, code, response_json_object, + response_code_message=None): # could alternatively use request.notifyFinish() and flip a flag when # the Deferred fires, but since the flag is RIGHT THERE it seems like # a waste. @@ -166,7 +168,8 @@ class JsonResource(HttpServer, resource.Resource): json_bytes = encode_pretty_printed_json(response_json_object) # TODO: Only enable CORS for the requests that need it. - respond_with_json_bytes(request, code, json_bytes, send_cors=True) + respond_with_json_bytes(request, code, json_bytes, send_cors=True, + response_code_message=response_code_message) @staticmethod def _request_user_agent_is_curl(request): @@ -350,7 +353,8 @@ class ContentRepoResource(resource.Resource): send_cors=True) -def respond_with_json_bytes(request, code, json_bytes, send_cors=False): +def respond_with_json_bytes(request, code, json_bytes, send_cors=False, + response_code_message=None): """Sends encoded JSON in response to the given request. Args: @@ -362,7 +366,7 @@ def respond_with_json_bytes(request, code, json_bytes, send_cors=False): Returns: twisted.web.server.NOT_DONE_YET""" - request.setResponseCode(code) + request.setResponseCode(code, message=response_code_message) request.setHeader(b"Content-Type", b"application/json") if send_cors: diff --git a/synapse/rest/room.py b/synapse/rest/room.py index a10b3b54f9..d76a2f5cd4 100644 --- a/synapse/rest/room.py +++ b/synapse/rest/room.py @@ -388,7 +388,7 @@ class RoomMembershipRestServlet(RestServlet): def register(self, http_server): # /rooms/$roomid/[invite|join|leave] PATTERN = ("/rooms/(?P<room_id>[^/]*)/" + - "(?P<membership_action>join|invite|leave)") + "(?P<membership_action>join|invite|leave|ban)") register_txn_path(self, PATTERN, http_server) @defer.inlineCallbacks @@ -399,7 +399,7 @@ class RoomMembershipRestServlet(RestServlet): # target user is you unless it is an invite state_key = user.to_string() - if membership_action == "invite": + if membership_action in ["invite", "ban"]: if "user_id" not in content: raise SynapseError(400, "Missing user_id key.") state_key = content["user_id"] diff --git a/synapse/server.py b/synapse/server.py index 3e72b2bcd5..35e311a47d 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -32,6 +32,7 @@ from synapse.util import Clock from synapse.util.distributor import Distributor from synapse.util.lockutils import LockManager from synapse.streams.events import EventSources +from synapse.api.ratelimiting import Ratelimiter class BaseHomeServer(object): @@ -73,6 +74,7 @@ class BaseHomeServer(object): 'resource_for_web_client', 'resource_for_content_repo', 'event_sources', + 'ratelimiter', ] def __init__(self, hostname, **kwargs): @@ -190,6 +192,9 @@ class HomeServer(BaseHomeServer): def build_event_sources(self): return EventSources(self) + def build_ratelimiter(self): + return Ratelimiter() + def register_servlets(self): """ Register all servlets associated with this HomeServer. """ diff --git a/synapse/storage/schema/room_aliases.sql b/synapse/storage/schema/room_aliases.sql index 71a8b90e4d..d72b79e6ed 100644 --- a/synapse/storage/schema/room_aliases.sql +++ b/synapse/storage/schema/room_aliases.sql @@ -1,3 +1,18 @@ +/* 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 room_aliases( room_alias TEXT NOT NULL, room_id TEXT NOT NULL diff --git a/synapse/util/logutils.py b/synapse/util/logutils.py index b94a749786..3d7c46ae44 100644 --- a/synapse/util/logutils.py +++ b/synapse/util/logutils.py @@ -19,7 +19,6 @@ from functools import wraps import logging import inspect -import traceback def log_function(f): diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/api/__init__.py diff --git a/tests/api/test_ratelimiting.py b/tests/api/test_ratelimiting.py new file mode 100644 index 0000000000..dc2f83c7eb --- /dev/null +++ b/tests/api/test_ratelimiting.py @@ -0,0 +1,39 @@ +from synapse.api.ratelimiting import Ratelimiter + +import unittest + +class TestRatelimiter(unittest.TestCase): + + def test_allowed(self): + limiter = Ratelimiter() + allowed, time_allowed = limiter.send_message( + user_id="test_id", time_now_s=0, msg_rate_hz=0.1, burst_count=1, + ) + self.assertTrue(allowed) + self.assertEquals(10., time_allowed) + + allowed, time_allowed = limiter.send_message( + user_id="test_id", time_now_s=5, msg_rate_hz=0.1, burst_count=1, + ) + self.assertFalse(allowed) + self.assertEquals(10., time_allowed) + + allowed, time_allowed = limiter.send_message( + user_id="test_id", time_now_s=10, msg_rate_hz=0.1, burst_count=1 + ) + self.assertTrue(allowed) + self.assertEquals(20., time_allowed) + + def test_pruning(self): + limiter = Ratelimiter() + allowed, time_allowed = limiter.send_message( + user_id="test_id_1", time_now_s=0, msg_rate_hz=0.1, burst_count=1, + ) + + self.assertIn("test_id_1", limiter.message_counts) + + allowed, time_allowed = limiter.send_message( + user_id="test_id_2", time_now_s=10, msg_rate_hz=0.1, burst_count=1 + ) + + self.assertNotIn("test_id_1", limiter.message_counts) diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py index 219a53c426..4591a5ea58 100644 --- a/tests/handlers/test_room.py +++ b/tests/handlers/test_room.py @@ -39,6 +39,10 @@ class RoomMemberHandlerTestCase(unittest.TestCase): hs = HomeServer( self.hostname, db_pool=None, + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), datastore=NonCallableMock(spec_set=[ "persist_event", "get_joined_hosts_for_room", @@ -82,6 +86,8 @@ class RoomMemberHandlerTestCase(unittest.TestCase): self.snapshot = Mock() self.datastore.snapshot_room.return_value = self.snapshot + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) @defer.inlineCallbacks def test_invite(self): @@ -342,6 +348,10 @@ class RoomCreationTest(unittest.TestCase): ]), auth=NonCallableMock(spec_set=["check"]), state_handler=NonCallableMock(spec_set=["handle_new_event"]), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), ) self.federation = NonCallableMock(spec_set=[ @@ -368,6 +378,9 @@ class RoomCreationTest(unittest.TestCase): return defer.succeed([]) self.datastore.get_joined_hosts_for_room.side_effect = hosts + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + @defer.inlineCallbacks def test_room_creation(self): user_id = "@foo:red" diff --git a/tests/rest/test_events.py b/tests/rest/test_events.py index 1d1336d12d..c8527f3517 100644 --- a/tests/rest/test_events.py +++ b/tests/rest/test_events.py @@ -32,7 +32,7 @@ import logging from ..utils import MockHttpResource, MemoryDataStore from .utils import RestTestCase -from mock import Mock +from mock import Mock, NonCallableMock logging.getLogger().addHandler(logging.NullHandler()) @@ -136,8 +136,15 @@ class EventStreamPermissionsTestCase(RestTestCase): "call_later", "cancel_call_later", "time_msec", + "time" ]), + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) hs.get_handlers().federation_handler = Mock() diff --git a/tests/rest/test_rooms.py b/tests/rest/test_rooms.py index cdaf948a3b..23c50824c7 100644 --- a/tests/rest/test_rooms.py +++ b/tests/rest/test_rooms.py @@ -30,7 +30,7 @@ import urllib from ..utils import MockHttpResource, MemoryDataStore from .utils import RestTestCase -from mock import Mock +from mock import Mock, NonCallableMock PATH_PREFIX = "/_matrix/client/api/v1" @@ -58,7 +58,14 @@ class RoomPermissionsTestCase(RestTestCase): replication_layer=Mock(), state_handler=state_handler, persistence_service=persistence_service, + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + hs.get_handlers().federation_handler = Mock() def _get_user_by_token(token=None): @@ -405,7 +412,14 @@ class RoomsMemberListTestCase(RestTestCase): replication_layer=Mock(), state_handler=state_handler, persistence_service=persistence_service, + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + hs.get_handlers().federation_handler = Mock() self.auth_user_id = self.user_id @@ -483,7 +497,14 @@ class RoomsCreateTestCase(RestTestCase): replication_layer=Mock(), state_handler=state_handler, persistence_service=persistence_service, + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + hs.get_handlers().federation_handler = Mock() def _get_user_by_token(token=None): @@ -573,7 +594,14 @@ class RoomTopicTestCase(RestTestCase): replication_layer=Mock(), state_handler=state_handler, persistence_service=persistence_service, + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + hs.get_handlers().federation_handler = Mock() def _get_user_by_token(token=None): @@ -676,7 +704,14 @@ class RoomMemberStateTestCase(RestTestCase): replication_layer=Mock(), state_handler=state_handler, persistence_service=persistence_service, + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + hs.get_handlers().federation_handler = Mock() def _get_user_by_token(token=None): @@ -801,7 +836,14 @@ class RoomMessagesTestCase(RestTestCase): replication_layer=Mock(), state_handler=state_handler, persistence_service=persistence_service, + ratelimiter=NonCallableMock(spec_set=[ + "send_message", + ]), + config=NonCallableMock(), ) + self.ratelimiter = hs.get_ratelimiter() + self.ratelimiter.send_message.return_value = (True, 0) + hs.get_handlers().federation_handler = Mock() def _get_user_by_token(token=None): diff --git a/webclient/app-filter.js b/webclient/app-filter.js index b8d3d2a0d8..e0e8130e45 100644 --- a/webclient/app-filter.js +++ b/webclient/app-filter.js @@ -103,7 +103,44 @@ angular.module('matrixWebClient') 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 (member.user_id in $rootScope.presence) { + // If the user is available in presence, use the displayname there + // as it is the most uptodate + roomName = $rootScope.presence[member.user_id].content.displayname; + } + else if (member.content.displayname) { + roomName = member.content.displayname; + } + else { + roomName = member.user_id; + } + } + } + } + else if (1 === Object.keys(room.members).length) { + // The other member may be in the invite list, get all invited users + var invitedUserIDs = []; + for (var i in room.messages) { + var message = room.messages[i]; + if ("m.room.member" === message.type && "invite" === message.membership) { + // Make sure there is no duplicate user + if (-1 === invitedUserIDs.indexOf(message.state_key)) { + invitedUserIDs.push(message.state_key); + } + } + } + + // For now, only 1:1 room needs to be renamed. It means only 1 invited user + if (1 === invitedUserIDs.length) { + var userID = invitedUserIDs[0]; + + // Try to resolve his displayname in presence global data + if (userID in $rootScope.presence) { + roomName = $rootScope.presence[userID].content.displayname; + } + else { + roomName = member.user_id; } } } diff --git a/webclient/app.css b/webclient/app.css index c27ec797a4..425d5bb11a 100755 --- a/webclient/app.css +++ b/webclient/app.css @@ -270,9 +270,9 @@ a:active { color: #000; } .userAvatar .userPowerLevel { position: absolute; - bottom: 20px; - height: 1px; - background-color: red; + bottom: 0px; + height: 2px; + background-color: #f00; } .userPresence { @@ -525,3 +525,13 @@ a:active { color: #000; } font-size: 24px; } +#user-displayname-input { + width: 160px; + max-width: 155px; +} + +#user-save-button { + width: 160px; + font-size: 14px; +} + diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 1f90472c67..72c290ad73 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -163,8 +163,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) // set target_user_id to keep things clear var target_user_id = chunk.state_key; - - var now = new Date().getTime(); var isNewMember = !(target_user_id in $scope.members); if (isNewMember) { @@ -174,6 +172,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) } if ("last_active_ago" in chunk.content) { chunk.last_active_ago = chunk.content.last_active_ago; + $scope.now = new Date().getTime(); + chunk.last_updated = $scope.now; } if ("displayname" in chunk.content) { chunk.displayname = chunk.content.displayname; @@ -181,7 +181,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) if ("avatar_url" in chunk.content) { chunk.avatar_url = chunk.content.avatar_url; } - chunk.last_updated = now; $scope.members[target_user_id] = chunk; if (target_user_id in $rootScope.presence) { @@ -197,6 +196,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) } if ("last_active_ago" in chunk.content) { member.last_active_ago = chunk.content.last_active_ago; + $scope.now = new Date().getTime(); + member.last_updated = $scope.now; } } }; @@ -221,6 +222,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) if ("last_active_ago" in chunk.content) { member.last_active_ago = chunk.content.last_active_ago; + $scope.now = new Date().getTime(); + member.last_updated = $scope.now; } // this may also contain a new display name or avatar url, so check. @@ -332,10 +335,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) eventHandlerService.waitForInitialSyncCompletion().then( function() { - // Some data has been retrieved from the iniialSync request - // So, the relative time starts here - $scope.now = new Date().getTime(); - var needsToJoin = true; // The room members is available in the data fetched by initialSync diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html index a69a8de300..03927838d2 100644 --- a/webclient/settings/settings.html +++ b/webclient/settings/settings.html @@ -12,11 +12,12 @@ <div class="profile-avatar"> <img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}" m-file-input="profile.avatarFile"/> </div> - <div id="user-ids"> - <input size="40" ng-model="profile.displayName" placeholder="Your display name"/> + <div> + <input id="user-displayname-input" size="40" ng-model="profile.displayName" placeholder="Your display name"/> <br/> - <button ng-disabled="(profile.displayName == profileOnServer.displayName) && (profile.avatarUrl == profileOnServer.avatarUrl)" - ng-click="saveProfile()">Save</button> + <button id="user-save-button" + ng-disabled="(profile.displayName === profileOnServer.displayName) && (profile.avatarUrl === profileOnServer.avatarUrl)" + ng-click="saveProfile()">Save changes</button> </div> </form> </div> |