diff --git a/cmdclient/console.py b/cmdclient/console.py
index a4d8145d72..7bda4000fc 100755
--- a/cmdclient/console.py
+++ b/cmdclient/console.py
@@ -225,8 +225,13 @@ class SynapseCmd(cmd.Cmd):
json_res = yield self.http_client.do_request("GET", url)
print json_res
- if ("type" not in json_res or "m.login.password" != json_res["type"] or
- "stages" in json_res):
+ if "flows" not in json_res:
+ print "Failed to find any login flows."
+ defer.returnValue(False)
+
+ flow = json_res["flows"][0] # assume first is the one we want.
+ if ("type" not in flow or "m.login.password" != flow["type"] or
+ "stages" in flow):
fallback_url = self._url() + "/login/fallback"
print ("Unable to login via the command line client. Please visit "
"%s to login." % fallback_url)
diff --git a/docs/code_style.rst b/docs/implementation-notes/code_style.rst
index d7e2d5e69e..d7e2d5e69e 100644
--- a/docs/code_style.rst
+++ b/docs/implementation-notes/code_style.rst
diff --git a/docs/documentation_style.rst b/docs/implementation-notes/documentation_style.rst
index c365d09dff..c365d09dff 100644
--- a/docs/documentation_style.rst
+++ b/docs/implementation-notes/documentation_style.rst
diff --git a/docs/python_architecture.rst b/docs/implementation-notes/python_architecture.rst
index 8beaa615d0..8beaa615d0 100644
--- a/docs/python_architecture.rst
+++ b/docs/implementation-notes/python_architecture.rst
diff --git a/docs/protocol_examples.rst b/docs/model/protocol_examples.rst
index 61a599b432..61a599b432 100644
--- a/docs/protocol_examples.rst
+++ b/docs/model/protocol_examples.rst
diff --git a/docs/terminology.rst b/docs/model/terminology.rst
index cc6e6760ac..cc6e6760ac 100644
--- a/docs/terminology.rst
+++ b/docs/model/terminology.rst
diff --git a/docs/versioning.rst b/docs/server-server/versioning.rst
index ffda60633f..ffda60633f 100644
--- a/docs/versioning.rst
+++ b/docs/server-server/versioning.rst
diff --git a/docs/specification.rst b/docs/specification.rst
new file mode 100644
index 0000000000..d650683efc
--- /dev/null
+++ b/docs/specification.rst
@@ -0,0 +1,804 @@
+Matrix Specification
+====================
+
+TODO(Introduction) : Matthew
+ - Similar to intro paragraph from README.
+ - Explaining the overall mission, what this spec describes...
+ - "What is Matrix?"
+ - Draw parallels with email?
+
+Architecture
+============
+- Sending a message from A to B
+
+::
+
+ How data flows between clients
+ ==============================
+
+ { Matrix client A } { Matrix client B }
+ ^ | ^ |
+ | events | | events |
+ | V | V
+ +------------------+ +------------------+
+ | |---------( HTTP )---------->| |
+ | Home Server | | Home Server |
+ | |<--------( HTTP )-----------| |
+ +------------------+ Federation +------------------+
+
+- Client is an end-user (web app, mobile app) which uses C-S APIs to talk to the home server.
+ A given client is typically responsible for a single user.
+- A single user is represented by a User ID, scoped to the home server which allocated the account.
+ User IDs MUST have @ prefix; looks like @foo:domain - domain indicates the user's home
+ server.
+- Home server provides C-S APIs and has the ability to federate with other HSes.
+ Typically responsible for N clients.
+- Federation's purpose is to share content between interested HSes; no SPOF.
+- Events are actions within the system. Typically each action (e.g. sending a message)
+ correlates with exactly one event. Each event has a ``type`` string.
+- ``type`` values SHOULD be namespaced according to standard Java package naming conventions,
+ with a ``.`` delimiter e.g. ``com.example.myapp.event``
+- Events are typically send in the context of a room.
+
+Room structure
+--------------
+
+A room is a conceptual place where users can send and receive messages. Rooms
+can be created, joined and left. Messages are sent to a room, and all
+participants in that room will receive the message. Rooms are uniquely
+identified via a room ID. There is exactly one room ID for each room. Each
+room can also have an alias. Each room can have many aliases.
+
+- Room IDs MUST have ! prefix; looks like !foo:domain - domain is simply for namespacing,
+ the room does NOT reside on any one domain. NOT human readable.
+
+- Room Aliases MUST have # prefix; looks like #foo:domain - domain indicates where this
+ alias can be mapped to a room ID. Key point: human readable / friendly.
+
+- Aliases can be queried on the domain they specify, which will return a room ID if a
+ mapping exists. These mappings can change.
+
+::
+
+ { @alice:matrix.org } { @bob:domain.com }
+ | ^
+ | |
+ Room ID: !qporfwt:matrix.org Room ID: !qporfwt:matrix.org
+ Event type: m.room.message Event type: m.room.message
+ Content: { JSON object } Content: { JSON object }
+ | |
+ V |
+ +------------------+ +------------------+
+ | Home Server | | Home Server |
+ | matrix.org |<-------Federation------->| domain.com |
+ +------------------+ +------------------+
+ | ................................. |
+ |______| Shared State |_______|
+ | Room ID: !qporfwt:matrix.org |
+ | Servers: matrix.org, domain.com |
+ | Members: |
+ | - @alice:matrix.org |
+ | - @bob:domain.com |
+ |.................................|
+
+- Federation's goal is to maintain the shared state. Don't need FULL state in order
+ to be a part of a room.
+- Introduce the DAG.
+- Events are wrapped in PDUs.
+
+
+Identity
+--------
+- Identity in relation to 3PIDs. Discovery of users based on 3PIDs.
+- Identity servers; trusted clique of servers which replicate content.
+- They govern the mapping of 3PIDs to user IDs and the creation of said mappings.
+- Not strictly required in order to communicate.
+
+
+API Standards
+-------------
+- All HTTP[S]
+- Uses JSON as HTTP bodies
+- Standard error response format { errcode: M_WHATEVER, error: "some message" }
+- C-S API provides POST for operations, or PUT with txn IDs. Explain txn IDs.
+
+Receiving live updates on a client
+----------------------------------
+- C-S longpoll event stream
+- Concept of start/end tokens.
+- Mention /initialSync to get token.
+
+
+Rooms
+=====
+- How are they created? PDU anchor point: "root of the tree".
+- Adding / removing aliases.
+- Invite/join dance
+- State and non-state data (+extensibility)
+
+TODO : Room permissions / config / power levels.
+
+Messages
+========
+
+This specification outlines several standard event types, all of which are
+prefixed with ``m.``
+
+State messages
+--------------
+- m.room.name
+- m.room.topic
+- m.room.member
+- m.room.config
+- m.room.invite_join
+
+What are they, when are they used, what do they contain, how should they be used
+
+Non-state messages
+------------------
+- m.room.message
+- m.room.message.feedback (and compressed format)
+
+What are they, when are they used, what do they contain, how should they be used
+
+m.room.message msgtypes
+-----------------------
+Each ``m.room.message`` MUST have a ``msgtype`` key which identifies the type of
+message being sent. Each type has their own required and optional keys, as outlined
+below:
+
+``m.text``
+ Required keys:
+ - ``body`` : "string" - The body of the message.
+ Optional keys:
+ None.
+ Example:
+ ``{ "msgtype": "m.text", "body": "I am a fish" }``
+
+``m.emote``
+ Required keys:
+ - ``body`` : "string" - The emote action to perform.
+ Optional keys:
+ None.
+ Example:
+ ``{ "msgtype": "m.emote", "body": "tries to come up with a witty explanation" }``
+
+``m.image``
+ Required keys:
+ - ``url`` : "string" - The URL to the image.
+ Optional keys:
+ - ``info`` : "string" - info : JSON object (ImageInfo) - The image info for image
+ referred to in ``url``.
+ - ``thumbnail_url`` : "string" - The URL to the thumbnail.
+ - ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
+ referred to in ``thumbnail_url``.
+ - ``body`` : "string" - The alt text of the image, or some kind of content
+ description for accessibility e.g. "image attachment".
+
+ ImageInfo:
+ Information about an image::
+
+ {
+ "size" : integer (size of image in bytes),
+ "w" : integer (width of image in pixels),
+ "h" : integer (height of image in pixels),
+ "mimetype" : "string (e.g. image/jpeg)",
+ }
+
+``m.audio``
+ Required keys:
+ - ``url`` : "string" - The URL to the audio.
+ Optional keys:
+ - ``info`` : JSON object (AudioInfo) - The audio info for the audio referred to in
+ ``url``.
+ - ``body`` : "string" - A description of the audio e.g. "Bee Gees -
+ Stayin' Alive", or some kind of content description for accessibility e.g.
+ "audio attachment".
+ AudioInfo:
+ Information about a piece of audio::
+
+ {
+ "mimetype" : "string (e.g. audio/aac)",
+ "size" : integer (size of audio in bytes),
+ "duration" : integer (duration of audio in milliseconds),
+ }
+
+``m.video``
+ Required keys:
+ - ``url`` : "string" - The URL to the video.
+ Optional keys:
+ - ``info`` : JSON object (VideoInfo) - The video info for the video referred to in
+ ``url``.
+ - ``body`` : "string" - A description of the video e.g. "Gangnam style",
+ or some kind of content description for accessibility e.g. "video attachment".
+
+ VideoInfo:
+ Information about a video::
+
+ {
+ "mimetype" : "string (e.g. video/mp4)",
+ "size" : integer (size of video in bytes),
+ "duration" : integer (duration of video in milliseconds),
+ "w" : integer (width of video in pixels),
+ "h" : integer (height of video in pixels),
+ "thumbnail_url" : "string (URL to image)",
+ "thumbanil_info" : JSON object (ImageInfo)
+ }
+
+``m.location``
+ Required keys:
+ - ``geo_uri`` : "string" - The geo URI representing the location.
+ Optional keys:
+ - ``thumbnail_url`` : "string" - The URL to a thumnail of the location being
+ represented.
+ - ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
+ referred to in ``thumbnail_url``.
+ - ``body`` : "string" - A description of the location e.g. "Big Ben,
+ London, UK", or some kind of content description for accessibility e.g.
+ "location attachment".
+
+The following keys can be attached to any ``m.room.message``:
+
+ Optional keys:
+ - ``sender_ts`` : integer - A timestamp (ms resolution) representing the
+ wall-clock time when the message was sent from the client.
+
+Presence
+========
+
+Each user has the concept of presence information. This encodes the
+"availability" of that user, suitable for display on other user's clients. This
+is transmitted as an ``m.presence`` event and is one of the few events which
+are sent *outside the context of a room*. The basic piece of presence information
+is represented by the ``state`` key, which is an enum of one of the following:
+
+ - ``online`` : The default state when the user is connected to an event stream.
+ - ``unavailable`` : The user is not reachable at this time.
+ - ``offline`` : The user is not connected to an event stream.
+ - ``free_for_chat`` : The user is generally willing to receive messages
+ moreso than default.
+ - ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
+ state anyway and generally interact with client features.
+
+This basic ``state`` field applies to the user as a whole, regardless of how many
+client devices they have connected. The home server should synchronise this
+status choice among multiple devices to ensure the user gets a consistent
+experience.
+
+Idle Time
+---------
+As well as the basic ``state`` field, the presence information can also show a sense
+of an "idle timer". This should be maintained individually by the user's
+clients, and the home server can take the highest reported time as that to
+report. When a user is offline, the home server can still report when the user was last
+seen online.
+
+Transmission
+------------
+- Transmitted as an EDU.
+- Presence lists determine who to send to.
+
+Presence List
+-------------
+Each user's home server stores a "presence list" for that user. This stores a
+list of other user IDs the user has chosen to add to it. To be added to this
+list, the user being added must receive permission from the list owner. Once
+granted, both user's HS(es) store this information. Since such subscriptions
+are likely to be bidirectional, HSes may wish to automatically accept requests
+when a reverse subscription already exists.
+
+Presence and Permissions
+------------------------
+For a viewing user to be allowed to see the presence information of a target
+user, either:
+
+ - The target user has allowed the viewing user to add them to their presence
+ list, or
+ - The two users share at least one room in common
+
+In the latter case, this allows for clients to display some minimal sense of
+presence information in a user list for a room.
+
+Typing notifications
+====================
+
+TODO : Leo
+
+Voice over IP
+=============
+
+TODO : Dave
+
+Profiles
+========
+
+Internally within Matrix users are referred to by their user ID, which is not a
+human-friendly string. Profiles grant users the ability to see human-readable
+names for other users that are in some way meaningful to them. Additionally,
+profiles can publish additional information, such as the user's age or location.
+
+A Profile consists of a display name, an avatar picture, and a set of other
+metadata fields that the user may wish to publish (email address, phone
+numbers, website URLs, etc...). This specification puts no requirements on the
+display name other than it being a valid unicode string.
+
+- Metadata extensibility
+- Bundled with which events? e.g. m.room.member
+- Generate own events? What type?
+
+Registration and login
+======================
+
+Clients must register with a home server in order to use Matrix. After
+registering, the client will be given an access token which must be used in ALL
+requests to that home server as a query parameter 'access_token'.
+
+- TODO Kegan : Make registration like login (just omit the "user" key on the
+ initial request?)
+
+If the client has already registered, they need to be able to login to their
+account. The home server may provide many different ways of logging in, such
+as user/password auth, login via a social network (OAuth2), login by confirming
+a token sent to their email address, etc. This specification does not define how
+home servers should authorise their users who want to login to their existing
+accounts, but instead defines the standard interface which implementations
+should follow so that ANY client can login to ANY home server.
+
+The login process breaks down into the following:
+ 1. Determine the requirements for logging in.
+ 2. Submit the login stage credentials.
+ 3. Get credentials or be told the next stage in the login process and repeat
+ step 2.
+
+As each home server may have different ways of logging in, the client needs to know how
+they should login. All distinct login stages MUST have a corresponding ``type``.
+A ``type`` is a namespaced string which details the mechanism for logging in.
+
+A client may be able to login via multiple valid login flows, and should choose a single
+flow when logging in. A flow is a series of login stages. The home server MUST respond
+with all the valid login flows when requested::
+
+ The client can login via 3 paths: 1a and 1b, 2a and 2b, or 3. The client should
+ select one of these paths.
+
+ {
+ "flows": [
+ {
+ "type": "<login type1a>",
+ "stages": [ "<login type 1a>", "<login type 1b>" ]
+ },
+ {
+ "type": "<login type2a>",
+ "stages": [ "<login type 2a>", "<login type 2b>" ]
+ },
+ {
+ "type": "<login type3>"
+ }
+ ]
+ }
+
+After the login is completed, the client's fully-qualified user ID and a new access
+token MUST be returned::
+
+ {
+ "user_id": "@user:matrix.org",
+ "access_token": "abcdef0123456789"
+ }
+
+The ``user_id`` key is particularly useful if the home server wishes to support
+localpart entry of usernames (e.g. "user" rather than "@user:matrix.org"), as the
+client may not be able to determine its ``user_id`` in this case.
+
+If a login has multiple requests, the home server may wish to create a session. If
+a home server responds with a 'session' key to a request, clients MUST submit it in
+subsequent requests until the login is completed::
+
+ {
+ "session": "<session id>"
+ }
+
+This specification defines the following login types:
+ - ``m.login.password``
+ - ``m.login.oauth2``
+ - ``m.login.email.code``
+ - ``m.login.email.url``
+
+
+Password-based
+--------------
+:Type:
+ m.login.password
+:Description:
+ Login is supported via a username and password.
+
+To respond to this type, reply with::
+
+ {
+ "type": "m.login.password",
+ "user": "<user_id or user localpart>",
+ "password": "<password>"
+ }
+
+The home server MUST respond with either new credentials, the next stage of the login
+process, or a standard error response.
+
+OAuth2-based
+------------
+:Type:
+ m.login.oauth2
+:Description:
+ Login is supported via OAuth2 URLs. This login consists of multiple requests.
+
+To respond to this type, reply with::
+
+ {
+ "type": "m.login.oauth2",
+ "user": "<user_id or user localpart>"
+ }
+
+The server MUST respond with::
+
+ {
+ "uri": <Authorization Request URI OR service selection URI>
+ }
+
+The home server acts as a 'confidential' client for the purposes of OAuth2.
+If the uri is a ``sevice selection URI``, it MUST point to a webpage which prompts the
+user to choose which service to authorize with. On selection of a service, this
+MUST link through to an ``Authorization Request URI``. If there is only 1 service which the
+home server accepts when logging in, this indirection can be skipped and the
+"uri" key can be the ``Authorization Request URI``.
+
+The client then visits the ``Authorization Request URI``, which then shows the OAuth2
+Allow/Deny prompt. Hitting 'Allow' returns the ``redirect URI`` with the auth code.
+Home servers can choose any path for the ``redirect URI``. The client should visit
+the ``redirect URI``, which will then finish the OAuth2 login process, granting the
+home server an access token for the chosen service. When the home server gets
+this access token, it verifies that the cilent has authorised with the 3rd party, and
+can now complete the login. The OAuth2 ``redirect URI`` (with auth code) MUST respond
+with either new credentials, the next stage of the login process, or a standard error
+response.
+
+For example, if a home server accepts OAuth2 from Google, it would return the
+Authorization Request URI for Google::
+
+ {
+ "uri": "https://accounts.google.com/o/oauth2/auth?response_type=code&
+ client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos"
+ }
+
+The client then visits this URI and authorizes the home server. The client then
+visits the REDIRECT_URI with the auth code= query parameter which returns::
+
+ {
+ "user_id": "@user:matrix.org",
+ "access_token": "0123456789abcdef"
+ }
+
+Email-based (code)
+------------------
+:Type:
+ m.login.email.code
+:Description:
+ Login is supported by typing in a code which is sent in an email. This login
+ consists of multiple requests.
+
+To respond to this type, reply with::
+
+ {
+ "type": "m.login.email.code",
+ "user": "<user_id or user localpart>",
+ "email": "<email address>"
+ }
+
+After validating the email address, the home server MUST send an email containing
+an authentication code and return::
+
+ {
+ "type": "m.login.email.code",
+ "session": "<session id>"
+ }
+
+The second request in this login stage involves sending this authentication code::
+
+ {
+ "type": "m.login.email.code",
+ "session": "<session id>",
+ "code": "<code in email sent>"
+ }
+
+The home server MUST respond to this with either new credentials, the next stage of
+the login process, or a standard error response.
+
+Email-based (url)
+-----------------
+:Type:
+ m.login.email.url
+:Description:
+ Login is supported by clicking on a URL in an email. This login consists of
+ multiple requests.
+
+To respond to this type, reply with::
+
+ {
+ "type": "m.login.email.url",
+ "user": "<user_id or user localpart>",
+ "email": "<email address>"
+ }
+
+After validating the email address, the home server MUST send an email containing
+an authentication URL and return::
+
+ {
+ "type": "m.login.email.url",
+ "session": "<session id>"
+ }
+
+The email contains a URL which must be clicked. After it has been clicked, the
+client should perform another request::
+
+ {
+ "type": "m.login.email.url",
+ "session": "<session id>"
+ }
+
+The home server MUST respond to this with either new credentials, the next stage of
+the login process, or a standard error response.
+
+A common client implementation will be to periodically poll until the link is clicked.
+If the link has not been visited yet, a standard error response with an errcode of
+``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned.
+
+
+N-Factor Authentication
+-----------------------
+Multiple login stages can be combined to create N-factor authentication during login.
+
+This can be achieved by responding with the ``next`` login type on completion of a
+previous login stage::
+
+ {
+ "next": "<next login type>"
+ }
+
+If a home server implements N-factor authentication, it MUST respond with all
+``stages`` when initially queried for their login requirements::
+
+ {
+ "type": "<1st login type>",
+ "stages": [ <1st login type>, <2nd login type>, ... , <Nth login type> ]
+ }
+
+This can be represented conceptually as::
+
+ _______________________
+ | Login Stage 1 |
+ | type: "<login type1>" |
+ | ___________________ |
+ | |_Request_1_________| | <-- Returns "session" key which is used throughout.
+ | ___________________ |
+ | |_Request_2_________| | <-- Returns a "next" value of "login type2"
+ |_______________________|
+ |
+ |
+ _________V_____________
+ | Login Stage 2 |
+ | type: "<login type2>" |
+ | ___________________ |
+ | |_Request_1_________| |
+ | ___________________ |
+ | |_Request_2_________| |
+ | ___________________ |
+ | |_Request_3_________| | <-- Returns a "next" value of "login type3"
+ |_______________________|
+ |
+ |
+ _________V_____________
+ | Login Stage 3 |
+ | type: "<login type3>" |
+ | ___________________ |
+ | |_Request_1_________| | <-- Returns user credentials
+ |_______________________|
+
+Fallback
+--------
+Clients cannot be expected to be able to know how to process every single
+login type. If a client determines it does not know how to handle a given
+login type, it should request a login fallback page::
+
+ GET matrix/client/api/v1/login/fallback
+
+This MUST return an HTML page which can perform the entire login process.
+
+Identity
+========
+
+TODO : Dave
+- 3PIDs and identity server, functions
+
+Federation
+==========
+
+Federation is the term used to describe how to communicate between Matrix home
+servers. Federation is a mechanism by which two home servers can exchange
+Matrix event messages, both as a real-time push of current events, and as a
+historic fetching mechanism to synchronise past history for clients to view. It
+uses HTTP connections between each pair of servers involved as the underlying
+transport. Messages are exchanged between servers in real-time by active pushing
+from each server's HTTP client into the server of the other. Queries to fetch
+historic data for the purpose of back-filling scrollback buffers and the like
+can also be performed.
+
+There are three main kinds of communication that occur between home servers:
+
+:Queries:
+ These are single request/response interactions between a given pair of
+ servers, initiated by one side sending an HTTP GET request to obtain some
+ information, and responded by the other. They are not persisted and contain
+ no long-term significant history. They simply request a snapshot state at the
+ instant the query is made.
+
+:Ephemeral Data Units (EDUs):
+ These are notifications of events that are pushed from one home server to
+ another. They are not persisted and contain no long-term significant history,
+ nor does the receiving home server have to reply to them.
+
+:Persisted Data Units (PDUs):
+ These are notifications of events that are broadcast from one home server to
+ any others that are interested in the same "context" (namely, a Room ID).
+ They are persisted to long-term storage and form the record of history for
+ that context.
+
+EDUs and PDUs are further wrapped in an envelope called a Transaction, which is
+transferred from the origin to the destination home server using an HTTP PUT request.
+
+
+Transactions
+------------
+The transfer of EDUs and PDUs between home servers is performed by an exchange
+of Transaction messages, which are encoded as JSON objects, passed over an
+HTTP PUT request. A Transaction is meaningful only to the pair of home servers that
+exchanged it; they are not globally-meaningful.
+
+Each transaction has:
+ - An opaque transaction ID.
+ - A timestamp (UNIX epoch time in milliseconds) generated by its origin server.
+ - An origin and destination server name.
+ - A list of "previous IDs".
+ - A list of PDUs and EDUs - the actual message payload that the Transaction carries.
+
+::
+
+ {
+ "transaction_id":"916d630ea616342b42e98a3be0b74113",
+ "ts":1404835423000,
+ "origin":"red",
+ "destination":"blue",
+ "prev_ids":["e1da392e61898be4d2009b9fecce5325"],
+ "pdus":[...],
+ "edus":[...]
+ }
+
+The ``prev_ids`` field contains a list of previous transaction IDs that
+the ``origin`` server has sent to this ``destination``. Its purpose is to act as a
+sequence checking mechanism - the destination server can check whether it has
+successfully received that Transaction, or ask for a retransmission if not.
+
+The ``pdus`` field of a transaction is a list, containing zero or more PDUs.[*]
+Each PDU is itself a JSON object containing a number of keys, the exact details of
+which will vary depending on the type of PDU. Similarly, the ``edus`` field is
+another list containing the EDUs. This key may be entirely absent if there are
+no EDUs to transfer.
+
+(* Normally the PDU list will be non-empty, but the server should cope with
+receiving an "empty" transaction, as this is useful for informing peers of other
+transaction IDs they should be aware of. This effectively acts as a push
+mechanism to encourage peers to continue to replicate content.)
+
+PDUs and EDUs
+-------------
+
+All PDUs have:
+ - An ID
+ - A context
+ - A declaration of their type
+ - A list of other PDU IDs that have been seen recently on that context (regardless of which origin
+ sent them)
+
+[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
+[origin,ref] pair like the prev_pdus are]]
+
+::
+
+ {
+ "pdu_id":"a4ecee13e2accdadf56c1025af232176",
+ "context":"#example.green",
+ "origin":"green",
+ "ts":1404838188000,
+ "pdu_type":"m.text",
+ "prev_pdus":[["blue","99d16afbc857975916f1d73e49e52b65"]],
+ "content":...
+ "is_state":false
+ }
+
+In contrast to Transactions, it is important to note that the ``prev_pdus``
+field of a PDU refers to PDUs that any origin server has sent, rather than
+previous IDs that this ``origin`` has sent. This list may refer to other PDUs sent
+by the same origin as the current one, or other origins.
+
+Because of the distributed nature of participants in a Matrix conversation, it
+is impossible to establish a globally-consistent total ordering on the events.
+However, by annotating each outbound PDU at its origin with IDs of other PDUs it
+has received, a partial ordering can be constructed allowing causallity
+relationships to be preserved. A client can then display these messages to the
+end-user in some order consistent with their content and ensure that no message
+that is semantically in reply of an earlier one is ever displayed before it.
+
+PDUs fall into two main categories: those that deliver Events, and those that
+synchronise State. For PDUs that relate to State synchronisation, additional
+keys exist to support this:
+
+::
+
+ {...,
+ "is_state":true,
+ "state_key":TODO
+ "power_level":TODO
+ "prev_state_id":TODO
+ "prev_state_origin":TODO}
+
+[[TODO(paul): At this point we should probably have a long description of how
+State management works, with descriptions of clobbering rules, power levels, etc
+etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
+so on. This part needs refining. And writing in its own document as the details
+relate to the server/system as a whole, not specifically to server-server
+federation.]]
+
+EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
+"previous" IDs. The only mandatory fields for these are the type, origin and
+destination home server names, and the actual nested content.
+
+::
+
+ {"edu_type":"m.presence",
+ "origin":"blue",
+ "destination":"orange",
+ "content":...}
+
+Backfilling
+-----------
+- What it is, when is it used, how is it done
+
+SRV Records
+-----------
+- Why it is needed
+
+Security
+========
+- rate limiting
+- crypto (s-s auth)
+- E2E
+- Lawful intercept + Key Escrow
+
+TODO Mark
+
+Policy Servers
+==============
+TODO
+
+Content repository
+==================
+- thumbnail paths
+
+Address book repository
+=======================
+- format
+
+
+Glossary
+========
+- domain specific words/acronyms with definitions
+
+User ID:
+ An opaque ID which identifies an end-user, which consists of some opaque
+ localpart combined with the domain name of their home server.
diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py
index 38ae360bcd..7868575a2e 100644
--- a/synapse/federation/replication.py
+++ b/synapse/federation/replication.py
@@ -541,7 +541,8 @@ class _TransactionQueue(object):
)
def eb(failure):
- deferred.errback(failure)
+ if not deferred.called:
+ deferred.errback(failure)
self._attempt_new_transaction(destination).addErrback(eb)
return deferred
diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py
index f141e92ce2..b37c8be964 100644
--- a/synapse/handlers/_base.py
+++ b/synapse/handlers/_base.py
@@ -35,7 +35,7 @@ class BaseRoomHandler(BaseHandler):
extra_users=[]):
snapshot.fill_out_prev_events(event)
- store_id = yield self.store.persist_event(event)
+ yield self.store.persist_event(event)
destinations = set(extra_destinations)
# Send a PDU to all hosts who have joined the room.
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 9023c3d403..eac110419c 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -22,8 +22,6 @@ from synapse.api.constants import Membership
from synapse.util.logutils import log_function
from synapse.federation.pdu_codec import PduCodec
-from synapse.api.errors import AuthError
-
from twisted.internet import defer
import logging
@@ -87,12 +85,6 @@ class FederationHandler(BaseHandler):
yield self.replication_layer.send_pdu(pdu)
@log_function
- def get_state_for_room(self, destination, room_id):
- return self.replication_layer.get_state_for_context(
- destination, room_id
- )
-
- @log_function
@defer.inlineCallbacks
def on_receive_pdu(self, pdu, backfilled):
""" Called by the ReplicationLayer when we have a new pdu. We need to
@@ -141,19 +133,19 @@ class FederationHandler(BaseHandler):
yield self.hs.get_handlers().room_member_handler.change_membership(
new_event,
- True
+ do_auth=True
)
else:
with (yield self.room_lock.lock(event.room_id)):
- store_id = yield self.store.persist_event(event, backfilled)
+ yield self.store.persist_event(event, backfilled)
room = yield self.store.get_room(event.room_id)
if not room:
# Huh, let's try and get the current state
try:
- yield self.get_state_for_room(
+ yield self.replication_layer.get_state_for_context(
event.origin, event.room_id
)
@@ -163,9 +155,9 @@ class FederationHandler(BaseHandler):
if self.hs.hostname in hosts:
try:
yield self.store.store_room(
- event.room_id,
- "",
- is_public=False
+ room_id=event.room_id,
+ room_creator_user_id="",
+ is_public=False,
)
except:
pass
@@ -188,27 +180,14 @@ class FederationHandler(BaseHandler):
@log_function
@defer.inlineCallbacks
def backfill(self, dest, room_id, limit):
- events = yield self._backfill(dest, room_id, limit)
-
- for event in events:
- try:
- yield self.store.persist_event(event, backfilled=True)
- except:
- logger.exception("Failed to persist event: %s", event)
-
- defer.returnValue(events)
-
- @defer.inlineCallbacks
- def _backfill(self, dest, room_id, limit):
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
- if not pdus:
- defer.returnValue([])
+ events = []
- events = [
- self.pdu_codec.event_from_pdu(pdu)
- for pdu in pdus
- ]
+ for pdu in pdus:
+ event = self.pdu_codec.event_from_pdu(pdu)
+ events.append(event)
+ yield self.store.persist_event(event, backfilled=True)
defer.returnValue(events)
@@ -224,7 +203,9 @@ class FederationHandler(BaseHandler):
# First get current state to see if we are already joined.
try:
- yield self.get_state_for_room(target_host, room_id)
+ yield self.replication_layer.get_state_for_context(
+ target_host, room_id
+ )
hosts = yield self.store.get_joined_hosts_for_room(room_id)
if self.hs.hostname in hosts:
@@ -254,8 +235,8 @@ class FederationHandler(BaseHandler):
try:
yield self.store.store_room(
- room_id,
- "",
+ room_id=room_id,
+ room_creator_user_id="",
is_public=False
)
except:
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index c479908f61..bef1508892 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -141,10 +141,6 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks
def is_presence_visible(self, observer_user, observed_user):
- defer.returnValue(True)
- return
- # FIXME (erikj): This code path absolutely kills the database.
-
assert(observed_user.is_mine)
if observer_user == observed_user:
@@ -189,10 +185,6 @@ class PresenceHandler(BaseHandler):
@defer.inlineCallbacks
def set_state(self, target_user, auth_user, state):
- return
- # TODO (erikj): Turn this back on. Why did we end up sending EDUs
- # everywhere?
-
if not target_user.is_mine:
raise SynapseError(400, "User is not hosted on this Home Server")
@@ -445,16 +437,22 @@ class PresenceHandler(BaseHandler):
)
def _start_polling_remote(self, user, domain, remoteusers):
+ to_poll = set()
+
for u in remoteusers:
if u not in self._remote_recvmap:
self._remote_recvmap[u] = set()
+ to_poll.add(u)
self._remote_recvmap[u].add(user)
+ if not to_poll:
+ return defer.succeed(None)
+
return self.federation.send_edu(
destination=domain,
edu_type="m.presence",
- content={"poll": [u.to_string() for u in remoteusers]}
+ content={"poll": [u.to_string() for u in to_poll]}
)
def stop_polling_presence(self, user, target_user=None):
@@ -497,16 +495,22 @@ class PresenceHandler(BaseHandler):
del self._local_pushmap[localpart]
def _stop_polling_remote(self, user, domain, remoteusers):
+ to_unpoll = set()
+
for u in remoteusers:
self._remote_recvmap[u].remove(user)
if not self._remote_recvmap[u]:
del self._remote_recvmap[u]
+ to_unpoll.add(u)
+
+ if not to_unpoll:
+ return defer.succeed(None)
return self.federation.send_edu(
destination=domain,
edu_type="m.presence",
- content={"unpoll": [u.to_string() for u in remoteusers]}
+ content={"unpoll": [u.to_string() for u in to_unpoll]}
)
@defer.inlineCallbacks
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index eb638fe50a..3e41d7a46b 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -325,7 +325,8 @@ class RoomMemberHandler(BaseRoomHandler):
)
snapshot = yield self.store.snapshot_room(
- room_id, joinee, RoomMemberEvent.TYPE, joinee
+ room_id, joinee.to_string(), RoomMemberEvent.TYPE,
+ joinee.to_string()
)
yield self._do_join(new_event, snapshot, room_host=host, do_auth=True)
diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py
index 9d38a7336e..9fab0ff37c 100644
--- a/synapse/handlers/typing.py
+++ b/synapse/handlers/typing.py
@@ -17,11 +17,12 @@ from twisted.internet import defer
from ._base import BaseHandler
+from synapse.api.errors import SynapseError, AuthError
+
import logging
from collections import namedtuple
-
logger = logging.getLogger(__name__)
diff --git a/synapse/rest/login.py b/synapse/rest/login.py
index bcf63fd2ab..99e4f10aac 100644
--- a/synapse/rest/login.py
+++ b/synapse/rest/login.py
@@ -27,7 +27,7 @@ class LoginRestServlet(RestServlet):
PASS_TYPE = "m.login.password"
def on_GET(self, request):
- return (200, {"type": LoginRestServlet.PASS_TYPE})
+ return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]})
def on_OPTIONS(self, request):
return (200, {})
diff --git a/synapse/server.py b/synapse/server.py
index ade8dc6c15..3e72b2bcd5 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -126,11 +126,6 @@ class BaseHomeServer(object):
object."""
return UserID.from_string(s, hs=self)
- def parse_roomid(self, s):
- """Parse the string given by 's' as a Room ID and return a RoomID
- object."""
- return RoomID.from_string(s, hs=self)
-
def parse_roomalias(self, s):
"""Parse the string given by 's' as a Room Alias and return a RoomAlias
object."""
diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py
index 4f42afc015..4945b0796e 100644
--- a/synapse/storage/stream.py
+++ b/synapse/storage/stream.py
@@ -294,7 +294,7 @@ class StreamStore(SQLBaseStore):
logger.debug("get_room_events_max_id: %s", res)
if not res or not res[0] or not res[0]["m"]:
- return "s1"
+ return "s0"
key = res[0]["m"]
return "s%d" % (key,)
diff --git a/synapse/streams/events.py b/synapse/streams/events.py
index eaa397c650..c68cf1a59c 100644
--- a/synapse/streams/events.py
+++ b/synapse/streams/events.py
@@ -15,7 +15,6 @@
from twisted.internet import defer
-from synapse.api.constants import Membership
from synapse.types import StreamToken
@@ -32,7 +31,7 @@ class NullSource(object):
return defer.succeed(0)
def get_pagination_rows(self, user, pagination_config, key):
- return defer.succeed(([], from_token))
+ return defer.succeed(([], pagination_config.from_token))
class RoomEventSource(object):
diff --git a/synapse/util/logutils.py b/synapse/util/logutils.py
index 9270a1790b..021649071b 100644
--- a/synapse/util/logutils.py
+++ b/synapse/util/logutils.py
@@ -15,6 +15,7 @@
from inspect import getcallargs
+from functools import wraps
import logging
@@ -26,6 +27,7 @@ def log_function(f):
lineno = f.func_code.co_firstlineno
pathname = f.func_code.co_filename
+ @wraps(f)
def wrapped(*args, **kwargs):
name = f.__module__
logger = logging.getLogger(name)
diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py
index bc260c8aab..fd19442645 100644
--- a/tests/handlers/test_federation.py
+++ b/tests/handlers/test_federation.py
@@ -28,6 +28,8 @@ from mock import NonCallableMock, ANY
import logging
+from ..utils import get_mock_call_args
+
logging.getLogger().addHandler(logging.NullHandler())
@@ -99,9 +101,13 @@ class FederationTestCase(unittest.TestCase):
mem_handler = self.handlers.room_member_handler
self.assertEquals(1, mem_handler.change_membership.call_count)
- self.assertEquals(True, mem_handler.change_membership.call_args[0][1])
+ call_args = get_mock_call_args(
+ lambda event, do_auth: None,
+ mem_handler.change_membership
+ )
+ self.assertEquals(True, call_args["do_auth"])
- new_event = mem_handler.change_membership.call_args[0][0]
+ new_event = call_args["event"]
self.assertEquals(RoomMemberEvent.TYPE, new_event.type)
self.assertEquals(room_id, new_event.room_id)
self.assertEquals(user_id, new_event.state_key)
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index 824ed07169..8d094fd1f9 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -15,7 +15,7 @@
from twisted.trial import unittest
-from twisted.internet import defer
+from twisted.internet import defer, reactor
from mock import Mock, call, ANY
import logging
@@ -192,7 +192,6 @@ class PresenceStateTestCase(unittest.TestCase):
),
SynapseError
)
- test_get_disallowed_state.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_set_my_state(self):
@@ -217,7 +216,6 @@ class PresenceStateTestCase(unittest.TestCase):
state={"state": OFFLINE})
self.mock_stop.assert_called_with(self.u_apple)
- test_set_my_state.skip = "Presence polling is disabled"
class PresenceInvitesTestCase(unittest.TestCase):
@@ -657,7 +655,6 @@ class PresencePushTestCase(unittest.TestCase):
observed_user=self.u_banana,
statuscache=ANY), # self-reflection
]) # and no others...
- test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_push_remote(self):
@@ -709,7 +706,6 @@ class PresencePushTestCase(unittest.TestCase):
)
yield put_json.await_calls()
- test_push_remote.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_recv_remote(self):
@@ -857,6 +853,7 @@ class PresencePollingTestCase(unittest.TestCase):
'apple': [ "@banana:test", "@clementine:test" ],
'banana': [ "@apple:test" ],
'clementine': [ "@apple:test", "@potato:remote" ],
+ 'fig': [ "@potato:remote" ],
}
@@ -906,9 +903,10 @@ class PresencePollingTestCase(unittest.TestCase):
# Mocked database state
# Local users always start offline
self.current_user_state = {
- "apple": OFFLINE,
- "banana": OFFLINE,
- "clementine": OFFLINE,
+ "apple": OFFLINE,
+ "banana": OFFLINE,
+ "clementine": OFFLINE,
+ "fig": OFFLINE,
}
def get_presence_state(user_localpart):
@@ -938,6 +936,7 @@ class PresencePollingTestCase(unittest.TestCase):
self.u_apple = hs.parse_userid("@apple:test")
self.u_banana = hs.parse_userid("@banana:test")
self.u_clementine = hs.parse_userid("@clementine:test")
+ self.u_fig = hs.parse_userid("@fig:test")
# Remote users
self.u_potato = hs.parse_userid("@potato:remote")
@@ -1002,7 +1001,6 @@ class PresencePollingTestCase(unittest.TestCase):
self.assertFalse("banana" in self.handler._local_pushmap)
self.assertFalse("clementine" in self.handler._local_pushmap)
- test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks
@@ -1028,10 +1026,32 @@ class PresencePollingTestCase(unittest.TestCase):
yield put_json.await_calls()
# Gut-wrenching tests
- self.assertTrue(self.u_potato in self.handler._remote_recvmap)
+ self.assertTrue(self.u_potato in self.handler._remote_recvmap,
+ msg="expected potato to be in _remote_recvmap"
+ )
self.assertTrue(self.u_clementine in
self.handler._remote_recvmap[self.u_potato])
+ # fig goes online; shouldn't send a second poll
+ yield self.handler.set_state(
+ target_user=self.u_fig, auth_user=self.u_fig,
+ state={"state": ONLINE}
+ )
+
+ reactor.iterate(delay=0)
+
+ put_json.assert_had_no_calls()
+
+ # fig goes offline
+ yield self.handler.set_state(
+ target_user=self.u_fig, auth_user=self.u_fig,
+ state={"state": OFFLINE}
+ )
+
+ reactor.iterate(delay=0)
+
+ put_json.assert_had_no_calls()
+
put_json.expect_call_and_return(
call("remote",
path="/matrix/federation/v1/send/1000001/",
@@ -1051,8 +1071,9 @@ class PresencePollingTestCase(unittest.TestCase):
put_json.await_calls()
- self.assertFalse(self.u_potato in self.handler._remote_recvmap)
- test_remote_poll_send.skip = "Presence polling is disabled"
+ self.assertFalse(self.u_potato in self.handler._remote_recvmap,
+ msg="expected potato not to be in _remote_recvmap"
+ )
@defer.inlineCallbacks
def test_remote_poll_receive(self):
diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py
index 1b106fc2b3..da06a06647 100644
--- a/tests/handlers/test_presencelike.py
+++ b/tests/handlers/test_presencelike.py
@@ -139,7 +139,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
mocked_set.assert_called_with("apple",
{"state": UNAVAILABLE, "status_msg": "Away"})
- test_set_my_state.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_push_local(self):
@@ -214,7 +213,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
"displayname": "I am an Apple",
"avatar_url": "http://foo",
}, statuscache.state)
- test_push_local.skip = "Presence polling is disabled"
@defer.inlineCallbacks
@@ -246,7 +244,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
],
},
)
- test_push_remote.skip = "Presence polling is disabled"
@defer.inlineCallbacks
def test_recv_remote(self):
diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py
index e15ee38741..7f7347dcf9 100644
--- a/tests/rest/test_presence.py
+++ b/tests/rest/test_presence.py
@@ -114,7 +114,6 @@ class PresenceStateTestCase(unittest.TestCase):
self.assertEquals(200, code)
mocked_set.assert_called_with("apple",
{"state": UNAVAILABLE, "status_msg": "Away"})
- test_set_my_status.skip = "Presence polling is disabled"
class PresenceListTestCase(unittest.TestCase):
@@ -318,4 +317,3 @@ class PresenceEventStreamTestCase(unittest.TestCase):
"mtime_age": 0,
}},
]}, response)
- test_shortpoll.skip = "Presence polling is disabled"
diff --git a/tests/utils.py b/tests/utils.py
index 6666b06931..98d4f9ed58 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -28,6 +28,16 @@ from mock import patch, Mock
import json
import urlparse
+from inspect import getcallargs
+
+
+def get_mock_call_args(pattern_func, mock_func):
+ """ Return the arguments the mock function was called with interpreted
+ by the pattern functions argument list.
+ """
+ invoked_args, invoked_kargs = mock_func.call_args
+ return getcallargs(pattern_func, *invoked_args, **invoked_kargs)
+
# This is a mock /resource/ not an entire server
class MockHttpResource(HttpServer):
@@ -238,8 +248,11 @@ class DeferredMockCallable(object):
def __init__(self):
self.expectations = []
+ self.calls = []
def __call__(self, *args, **kwargs):
+ self.calls.append((args, kwargs))
+
if not self.expectations:
raise ValueError("%r has no pending calls to handle call(%s)" % (
self, _format_call(args, kwargs))
@@ -262,3 +275,15 @@ class DeferredMockCallable(object):
while self.expectations:
(_, _, d) = self.expectations.pop(0)
yield d
+ self.calls = []
+
+ def assert_had_no_calls(self):
+ if self.calls:
+ calls = self.calls
+ self.calls = []
+
+ raise AssertionError("Expected not to received any calls, got:\n" +
+ "\n".join([
+ "call(%s)" % _format_call(c[0], c[1]) for c in calls
+ ])
+ )
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 5d3fa6ddc8..80474bb8df 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -33,7 +33,7 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
});
if (matrixService.isUserLoggedIn()) {
- // eventStreamService.resume();
+ eventStreamService.resume();
mPresence.start();
}
diff --git a/webclient/app.js b/webclient/app.js
index b52479babe..02695c3ae6 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -81,13 +81,11 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
$httpProvider.interceptors.push('AccessTokenInterceptor');
}]);
-matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', function($location, matrixService, eventStreamService) {
+matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) {
+
// If user auth details are not in cache, go to the login page
if (!matrixService.isUserLoggedIn()) {
- eventStreamService.stop();
$location.path("login");
}
- else {
- // eventStreamService.resume();
- }
+
}]);
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index 7514770583..2f7580d682 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -27,13 +27,15 @@ Typically, this service will store events or broadcast them to any listeners
if typically all the $on method would do is update its own $scope.
*/
angular.module('eventHandlerService', [])
-.factory('eventHandlerService', ['matrixService', '$rootScope', function(matrixService, $rootScope) {
+.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
var MSG_EVENT = "MSG_EVENT";
var MEMBER_EVENT = "MEMBER_EVENT";
var PRESENCE_EVENT = "PRESENCE_EVENT";
+
+ var InitialSyncDeferred = $q.defer();
$rootScope.events = {
- rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
+ rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
};
$rootScope.presence = {};
@@ -47,11 +49,11 @@ angular.module('eventHandlerService', [])
}
}
- var reInitRoom = function(room_id) {
- $rootScope.events.rooms[room_id] = {};
- $rootScope.events.rooms[room_id].messages = [];
- $rootScope.events.rooms[room_id].members = {};
- }
+ var resetRoomMessages = function(room_id) {
+ if ($rootScope.events.rooms[room_id]) {
+ $rootScope.events.rooms[room_id].messages = [];
+ }
+ };
var handleMessage = function(event, isLiveEvent) {
initRoom(event.room_id);
@@ -124,8 +126,18 @@ angular.module('eventHandlerService', [])
}
},
- reInitRoom: function(room_id) {
- reInitRoom(room_id);
+ handleInitialSyncDone: function() {
+ console.log("# handleInitialSyncDone");
+ InitialSyncDeferred.resolve($rootScope.events, $rootScope.presence);
},
+
+ // Returns a promise that resolves when the initialSync request has been processed
+ waitForInitialSyncCompletion: function() {
+ return InitialSyncDeferred.promise;
+ },
+
+ resetRoomMessages: function(room_id) {
+ resetRoomMessages(room_id);
+ }
};
}]);
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index a1a98b2a36..441148670e 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -25,7 +25,8 @@ the eventHandlerService.
angular.module('eventStreamService', [])
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
var END = "END";
- var TIMEOUT_MS = 30000;
+ var SERVER_TIMEOUT_MS = 30000;
+ var CLIENT_TIMEOUT_MS = 40000;
var ERR_TIMEOUT_MS = 5000;
var settings = {
@@ -55,7 +56,7 @@ angular.module('eventStreamService', [])
deferred = deferred || $q.defer();
// run the stream from the latest token
- matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
+ matrixService.getEventStream(settings.from, SERVER_TIMEOUT_MS, CLIENT_TIMEOUT_MS).then(
function(response) {
if (!settings.isActive) {
console.log("[EventStream] Got response but now inactive. Dropping data.");
@@ -80,7 +81,7 @@ angular.module('eventStreamService', [])
}
},
function(error) {
- if (error.status == 403) {
+ if (error.status === 403) {
settings.shouldPoll = false;
}
@@ -96,7 +97,7 @@ angular.module('eventStreamService', [])
);
return deferred.promise;
- }
+ };
var startEventStream = function() {
settings.shouldPoll = true;
@@ -110,18 +111,17 @@ angular.module('eventStreamService', [])
for (var i = 0; i < rooms.length; ++i) {
var room = rooms[i];
if ("state" in room) {
- for (var j = 0; j < room.state.length; ++j) {
- eventHandlerService.handleEvents(room.state[j], false);
- }
+ eventHandlerService.handleEvents(room.state, false);
}
}
var presence = response.data.presence;
- for (var i = 0; i < presence.length; ++i) {
- eventHandlerService.handleEvent(presence[i], false);
- }
+ eventHandlerService.handleEvents(presence, false);
+
+ // Initial sync is done
+ eventHandlerService.handleInitialSyncDone();
- settings.from = response.data.end
+ settings.from = response.data.end;
doEventStream(deferred);
},
function(error) {
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index 2feddac5d8..b56eef6af5 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -41,7 +41,7 @@ angular.module('matrixService', [])
var prefixPath = "/matrix/client/api/v1";
var MAPPING_PREFIX = "alias_for_";
- var doRequest = function(method, path, params, data) {
+ var doRequest = function(method, path, params, data, $httpParams) {
if (!config) {
console.warn("No config exists. Cannot perform request to "+path);
return;
@@ -58,7 +58,7 @@ angular.module('matrixService', [])
path = prefixPath + path;
}
- return doBaseRequest(config.homeserver, method, path, params, data, undefined);
+ return doBaseRequest(config.homeserver, method, path, params, data, undefined, $httpParams);
};
var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
@@ -343,15 +343,31 @@ angular.module('matrixService', [])
return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
},
-
- // start listening on /events
- getEventStream: function(from, timeout) {
+
+ /**
+ * Start listening on /events
+ * @param {String} from the token from which to listen events to
+ * @param {Integer} serverTimeout the time in ms the server will hold open the connection
+ * @param {Integer} clientTimeout the timeout in ms used at the client HTTP request level
+ * @returns a promise
+ */
+ getEventStream: function(from, serverTimeout, clientTimeout) {
var path = "/events";
var params = {
from: from,
- timeout: timeout
+ timeout: serverTimeout
};
- return doRequest("GET", path, params);
+
+ var $httpParams;
+ if (clientTimeout) {
+ // If the Internet connection is lost, this timeout is used to be able to
+ // cancel the current request and notify the client so that it can retry with a new request.
+ $httpParams = {
+ timeout: clientTimeout
+ };
+ }
+
+ return doRequest("GET", path, params, undefined, $httpParams);
},
// Indicates if user authentications details are stored in cache
@@ -420,34 +436,38 @@ angular.module('matrixService', [])
/****** Room aliases management ******/
/**
- * Enhance data returned by rooms() and publicRooms() by adding room_alias
- * & room_display_name which are computed from data already retrieved from the server.
- * @param {Array} data the response of rooms() and publicRooms()
- * @returns {Array} the same array with enriched objects
+ * Get the room_alias & room_display_name which are computed from data
+ * already retrieved from the server.
+ * @param {Room object} room one element of the array returned by the response
+ * of rooms() and publicRooms()
+ * @returns {Object} {room_alias: "...", room_display_name: "..."}
*/
- assignRoomAliases: function(data) {
- for (var i=0; i<data.length; i++) {
- var alias = this.getRoomIdToAliasMapping(data[i].room_id);
- if (alias) {
- // use the existing alias from storage
- data[i].room_alias = alias;
- data[i].room_display_name = alias;
- }
- else if (data[i].aliases && data[i].aliases[0]) {
- // save the mapping
- // TODO: select the smarter alias from the array
- this.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
- data[i].room_display_name = data[i].aliases[0];
- }
- else if (data[i].membership == "invite" && "inviter" in data[i]) {
- data[i].room_display_name = data[i].inviter + "'s room"
- }
- else {
- // last resort use the room id
- data[i].room_display_name = data[i].room_id;
- }
+ getRoomAliasAndDisplayName: function(room) {
+ var result = {
+ room_alias: undefined,
+ room_display_name: undefined
+ };
+
+ var alias = this.getRoomIdToAliasMapping(room.room_id);
+ if (alias) {
+ // use the existing alias from storage
+ result.room_alias = alias;
+ result.room_display_name = alias;
+ }
+ else if (room.aliases && room.aliases[0]) {
+ // save the mapping
+ // TODO: select the smarter alias from the array
+ this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]);
+ result.room_display_name = room.aliases[0];
+ }
+ else if (room.membership === "invite" && "inviter" in room) {
+ result.room_display_name = room.inviter + "'s room";
+ }
+ else {
+ // last resort use the room id
+ result.room_display_name = room.room_id;
}
- return data;
+ return result;
},
createRoomIdToAliasMapping: function(roomId, alias) {
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
index e8e91eede7..547a5c5603 100644
--- a/webclient/home/home-controller.js
+++ b/webclient/home/home-controller.js
@@ -17,8 +17,8 @@ limitations under the License.
'use strict';
angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
-.controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService', 'eventStreamService',
- function($scope, $location, matrixService, eventHandlerService, eventStreamService) {
+.controller('HomeController', ['$scope', '$location', 'matrixService',
+ function($scope, $location, matrixService) {
$scope.config = matrixService.config();
$scope.public_rooms = [];
@@ -42,11 +42,15 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
matrixService.publicRooms().then(
function(response) {
- $scope.public_rooms = matrixService.assignRoomAliases(response.data.chunk);
+ $scope.public_rooms = response.data.chunk;
+ for (var i = 0; i < $scope.public_rooms.length; i++) {
+ var room = $scope.public_rooms[i];
+
+ // Add room_alias & room_display_name members
+ angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
+ }
}
);
-
- eventStreamService.resume();
};
$scope.createNewRoom = function(room_id, isPrivate) {
diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js
index bf6a1b8874..d33d41a922 100644
--- a/webclient/recents/recents-controller.js
+++ b/webclient/recents/recents-controller.js
@@ -17,21 +17,34 @@
'use strict';
angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
-.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService', 'eventStreamService',
- function($scope, matrixService, eventHandlerService, eventStreamService) {
+.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService',
+ function($scope, matrixService, eventHandlerService) {
$scope.rooms = {};
// $scope of the parent where the recents component is included can override this value
// in order to highlight a specific room in the list
$scope.recentsSelectedRoomID;
- // Refresh the list on matrix invitation and message event
- $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
- refresh();
- });
- $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
- refresh();
- });
+ var listenToEventStream = function() {
+ // Refresh the list on matrix invitation and message event
+ $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
+ var config = matrixService.config();
+ if (isLive && event.state_key === config.user_id && event.content.membership === "invite") {
+ console.log("Invited to room " + event.room_id);
+ // FIXME push membership to top level key to match /im/sync
+ event.membership = event.content.membership;
+ // FIXME bodge a nicer name than the room ID for this invite.
+ event.room_display_name = event.user_id + "'s room";
+ $scope.rooms[event.room_id] = event;
+ }
+ });
+ $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
+ if (isLive) {
+ $scope.rooms[event.room_id].lastMsg = event;
+ }
+ });
+ };
+
var refresh = function() {
// List all rooms joined or been invited to
@@ -42,13 +55,16 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
// Reset data
$scope.rooms = {};
- var data = matrixService.assignRoomAliases(response.data.rooms);
- for (var i=0; i<data.length; i++) {
- $scope.rooms[data[i].room_id] = data[i];
+ var rooms = response.data.rooms;
+ for (var i=0; i<rooms.length; i++) {
+ var room = rooms[i];
+
+ // Add room_alias & room_display_name members
+ $scope.rooms[room.room_id] = angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
// Create a shortcut for the last message of this room
- if (data[i].messages && data[i].messages.chunk && data[i].messages.chunk[0]) {
- $scope.rooms[data[i].room_id].lastMsg = data[i].messages.chunk[0];
+ if (room.messages && room.messages.chunk && room.messages.chunk[0]) {
+ $scope.rooms[room.room_id].lastMsg = room.messages.chunk[0];
}
}
@@ -56,6 +72,9 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
for (var i = 0; i < presence.length; ++i) {
eventHandlerService.handleEvent(presence[i], false);
}
+
+ // From now, update recents from the stream
+ listenToEventStream();
},
function(error) {
$scope.feedback = "Failure: " + error.data;
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index 6fda6c5c6b..3f025a98d8 100644
--- a/webclient/recents/recents.html
+++ b/webclient/recents/recents.html
@@ -39,6 +39,11 @@
{{ room.lastMsg.user_id }} sent an image
</div>
+ <div ng-switch-when="m.emote">
+ <span ng-bind-html="'* ' + (room.lastMsg.user_id) + ' ' + room.lastMsg.content.body | linky:'_blank'">
+ </span>
+ </div>
+
<div ng-switch-default>
{{ room.lastMsg.content }}
</div>
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index c596af820c..15710d2ba3 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
-.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService', 'mFileUpload', 'MatrixCall', 'mUtilities', '$rootScope',
- function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, matrixPhoneService, mFileUpload, MatrixCall, mUtilities, $rootScope) {
+angular.module('RoomController', ['ngSanitize', 'mFileInput'])
+.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
+ function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
'use strict';
var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320;
@@ -298,7 +298,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
}
if (room_id_or_alias && '!' === room_id_or_alias[0]) {
- // Yes. We can start right now
+ // Yes. We can go on right now
$scope.room_id = room_id_or_alias;
$scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
onInit2();
@@ -329,7 +329,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
$scope.room_id = response.data.room_id;
console.log(" -> Room ID: " + $scope.room_id);
- // Now, we can start
+ // Now, we can go on
onInit2();
},
function () {
@@ -339,37 +339,61 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
});
}
};
-
+
var onInit2 = function() {
- eventHandlerService.reInitRoom($scope.room_id);
-
- // Make recents highlight the current room
- $scope.recentsSelectedRoomID = $scope.room_id;
-
- // Join the room
- matrixService.join($scope.room_id).then(
+ console.log("onInit2");
+
+ // Make sure the initialSync has been before going further
+ eventHandlerService.waitForInitialSyncCompletion().then(
function() {
- console.log("Joined room "+$scope.room_id);
+ var needsToJoin = true;
+
+ // The room members is available in the data fetched by initialSync
+ if ($rootScope.events.rooms[$scope.room_id]) {
+ var members = $rootScope.events.rooms[$scope.room_id].members;
+
+ // Update the member list
+ for (var i in members) {
+ var member = members[i];
+ updateMemberList(member);
+ }
- // Get the current member list
- matrixService.getMemberList($scope.room_id).then(
- function(response) {
- for (var i = 0; i < response.data.chunk.length; i++) {
- var chunk = response.data.chunk[i];
- updateMemberList(chunk);
+ // Check if the user has already join the room
+ if ($scope.state.user_id in members) {
+ if ("join" === members[$scope.state.user_id].membership) {
+ needsToJoin = false;
}
- eventStreamService.resume();
- },
- function(error) {
- $scope.feedback = "Failed get member list: " + error.data.error;
}
- );
+ }
- paginate(MESSAGES_PER_PAGINATION);
- },
- function(reason) {
- $scope.feedback = "Can't join room: " + reason;
- });
+ // Do we to join the room before starting?
+ if (needsToJoin) {
+ matrixService.join($scope.room_id).then(
+ function() {
+ console.log("Joined room "+$scope.room_id);
+ onInit3();
+ },
+ function(reason) {
+ $scope.feedback = "Can't join room: " + reason;
+ });
+ }
+ else {
+ onInit3();
+ }
+ }
+ );
+ };
+
+ var onInit3 = function() {
+ console.log("onInit3");
+
+ // TODO: We should be able to keep them
+ eventHandlerService.resetRoomMessages($scope.room_id);
+
+ // Make recents highlight the current room
+ $scope.recentsSelectedRoomID = $scope.room_id;
+
+ paginate(MESSAGES_PER_PAGINATION);
};
$scope.inviteUser = function(user_id) {
diff --git a/webclient/room/room.html b/webclient/room/room.html
index bc3eefaf34..572c52e64e 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -45,13 +45,13 @@
</td>
<td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
<div class="bubble">
- <span ng-hide='msg.type !== "m.room.member"'>
+ <span ng-show='msg.type === "m.room.member"'>
{{ members[msg.user_id].displayname || msg.user_id }}
{{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
{{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }}
</span>
- <span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
- <span ng-hide='msg.content.msgtype !== "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
+ <span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
+ <span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
<div ng-show='msg.content.msgtype === "m.image"'>
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
<img class="image" ng-src="{{ msg.content.url }}"/>
@@ -109,7 +109,7 @@
</div>
{{ feedback }}
- <div ng-hide="!state.stream_failure">
+ <div ng-show="state.stream_failure">
{{ state.stream_failure.data.error || "Connection failure" }}
</div>
</div>
|